OpenCode 实践课:4 分钟用 Go 写出一个命令行 Key-Value 存储服务

项目介绍

今天我们用 OpenCode 写一个基于 HTTP 的 Key-Value 存储服务。它提供 RESTful API,支持 GET/PUT/DELETE 操作,使用内存存储,线程安全,约 100 行代码。

技术栈:Go 1.21+,仅用标准库(net/http + sync),无需任何第三方依赖。最终效果:一个可通过 curl 交互的轻量 KV 服务器。

准备工作

  • 安装 Go(1.21 以上版本)
  • 安装 OpenCode:npm install -g @anthropic/opencode(或参考 opencode.ai 文档)
  • 终端进入一个空目录

实践过程

第一步:初始化项目骨架

我对 OpenCode 说:

> 帮我创建一个 Go 项目,初始化 go mod,创建 main.go,main 函数里启动一个 HTTP 服务器监听 :8080,并打印启动日志。

OpenCode 自动执行 go mod init kvstore,生成 main.go

package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello"))
    })

    log.Println("KV Store running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

运行 go run main.go,服务直接跑起来了。关键要点:OpenCode 不仅是代码补全——它会帮你完成从模块初始化到代码生成的全流程。

第二步:实现线程安全的内存存储引擎

我对 OpenCode 说:

> 添加一个 Store 结构体,内部用 map[string]string 存数据,带 sync.RWMutex 保证并发安全。实现 Get、Set、Delete、List 四个方法。

OpenCode 在 main() 上方插入代码:

type Store struct {
    mu   sync.RWMutex
    data map[string]string
}

func NewStore() *Store {
    return &Store{data: make(map[string]string)}
}

func (s *Store) Get(key string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.data[key]
    return v, ok
}

func (s *Store) Set(key, value string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.data[key] = value
}

func (s *Store) Delete(key string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.data[key]
    if ok {
        delete(s.data, key)
    }
    return ok
}

func (s *Store) List() map[string]string {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make(map[string]string)
    for k, v := range s.data {
        result[k] = v
    }
    return result
}

关键要点:注意 OpenCode 自动处理了并发安全问题——Get/List 用读锁,Set/Delete 用写锁,Go 程序员常犯的 map 并发写 panic 被直接规避了。

第三步:挂载 RESTful API

我对 OpenCode 说:

> 把 /api/kv/ 路径注册为 KV 操作的 handler:GET /api/kv/{key} 返回 JSON;PUT /api/kv/{key} 从请求体 {"value":"..."} 读取并写入;DELETE /api/kv/{key} 删除。再加一个 GET /api/kv 列出所有键值对。

OpenCode 替换了原来的 handler,生成路由分发逻辑:

http.HandleFunc("/api/kv/", func(w http.ResponseWriter, r *http.Request) {
    key := strings.TrimPrefix(r.URL.Path, "/api/kv/")
    if key == "" {
        // 列出全部 - 但 /api/kv/ 末尾有斜线
        // 实际全部列表用 /api/kv
        http.Error(w, "key required", http.StatusBadRequest)
        return
    }
    switch r.Method {
    case http.MethodGet:
        v, ok := store.Get(key)
        if !ok {
            http.Error(w, "not found", http.StatusNotFound)
            return
        }
        json.NewEncoder(w).Encode(map[string]string{"key": key, "value": v})
    case http.MethodPut:
        var body map[string]string
        json.NewDecoder(r.Body).Decode(&body)
        store.Set(key, body["value"])
        fmt.Fprintf(w, `{"status":"ok"}`)
    case http.MethodDelete:
        store.Delete(key)
        fmt.Fprintf(w, `{"status":"ok"}`)
    }
})

关键要点:我只描述了 API 行为,OpenCode 自行处理了路由解析、错误返回、JSON 编解码。强调"干什么"而非"怎么干",是高效使用 OpenCode 的核心技巧。

第四步:补充健壮性并测试

我对 OpenCode 说:

> 给所有端点加上 Content-Type: application/json 响应头,PUT 时 body.value 为空返回 400,DELETE 不存在的 key 返回 404,不支持的方法返回 405。最后帮我写出 curl 测试命令。

OpenCode 完善了所有错误分支,并输出测试用例:

curl -X PUT http://localhost:8080/api/kv/name -d '{"value":"Alice"}'
# {"status":"ok"}

curl http://localhost:8080/api/kv/name
# {"key":"name","value":"Alice"}

curl -X DELETE http://localhost:8080/api/kv/name
# {"status":"ok"}

curl http://localhost:8080/api/kv
# {}

关键要点:这一轮体现了 OpenCode 的"迭代精化"能力——第一版能用就行,第二轮补齐边界条件。实际开发中这非常省时间,你只需指出缺陷,AI 负责修复。

完整代码

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"
    "sync"
)

type Store struct {
    mu   sync.RWMutex
    data map[string]string
}

func NewStore() *Store {
    return &Store{data: make(map[string]string)}
}

func (s *Store) Get(key string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.data[key]
    return v, ok
}

func (s *Store) Set(key, value string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.data[key] = value
}

func (s *Store) Delete(key string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.data[key]
    if ok {
        delete(s.data, key)
    }
    return ok
}

func (s *Store) List() map[string]string {
    s.mu.RLock()
    defer s.mu.RUnlock()
    m := make(map[string]string)
    for k, v := range s.data {
        m[k] = v
    }
    return m
}

func main() {
    store := NewStore()

    http.HandleFunc("/api/kv/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        key := strings.TrimPrefix(r.URL.Path, "/api/kv/")
        if key == "" {
            http.Error(w, `{"error":"key required"}`, http.StatusBadRequest)
            return
        }
        switch r.Method {
        case http.MethodGet:
            v, ok := store.Get(key)
            if !ok {
                http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
                return
            }
            json.NewEncoder(w).Encode(map[string]string{"key": key, "value": v})
        case http.MethodPut:
            var body map[string]string
            if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
                http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
                return
            }
            val, ok := body["value"]
            if !ok || val == "" {
                http.Error(w, `{"error":"value field required"}`, http.StatusBadRequest)
                return
            }
            store.Set(key, val)
            fmt.Fprint(w, `{"status":"ok"}`)
        case http.MethodDelete:
            if !store.Delete(key) {
                http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
                return
            }
            fmt.Fprint(w, `{"status":"ok"}`)
        default:
            http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
        }
    })

    http.HandleFunc("/api/kv", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        if r.Method != http.MethodGet {
            http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
            return
        }
        json.NewEncoder(w).Encode(store.List())
    })

    addr := ":8080"
    log.Printf("KV Store running on %s", addr)
    log.Fatal(http.ListenAndServe(addr, nil))
}

小结

整个开发过程约 4 分钟,四轮对话,每轮一句话描述需求即可。

传统写一个带并发安全的 REST 服务,你需要手动查 API 文档、处理锁的读写分离、写 JSON 编解码模板、补齐一堆错误分支——不说半小时,十分钟总要的。用 OpenCode 基本就是"描述业务 → 验证运行 → 补充边界 → 收工"。

最让我印象深刻的是 OpenCode 对项目上下文的感知能力:第三步加路由时它知道 store 变量已经存在,第四步补边界时知道所有端点的上下文。不是那种"一次性 prompt 生成",而是真正在陪你迭代开发

如果这篇文章对你有帮助,欢迎在评论区交流你用 OpenCode 的体验。