OpenCode 实践课:4 分钟用 Go 写出一个命令行 Pastebin 分享服务

项目介绍

在日常开发中,我们经常需要快速分享一段代码或文本给同事——可能是调试日志、配置文件片段,或者一段示例代码。每次都打开 GitHub Gist 或 Pastebin 网站太繁琐,不如自己搭一个本地的 Pastebin 服务。

本文将演示如何用 OpenCode 在 4 分钟内用 Go 写一个轻量级的 Pastebin 服务。项目包含:

  • RESTful API:支持 POST 创建粘贴、GET 查看粘贴
  • Web 界面:带暗色主题的代码提交页面
  • 语法类型标注:支持多种编程语言分类
  • 并发安全:读写锁保护内存存储

技术栈:Go 标准库,零依赖,最终代码 82 行。

准备工作

安装好 OpenCode 后,在终端进入项目目录,启动 OpenCode 会话即可开始。

实践过程

步骤一:用自然语言描述需求,生成基础骨架

我对 OpenCode 说的第一句话:

> 帮我用 Go 写一个 Pastebin 服务,运行在 8080 端口。支持 POST 提交文本,返回一个唯一的分享链接;GET 访问链接时展示文本内容。数据存在内存里就行,用标准库,不要第三方依赖。

OpenCode 分析了需求后,生成了如下结构:

  • crypto/rand + hex 生成 8 位随机 ID
  • map[string]Paste 做内存存储
  • sync.RWMutex 保护并发读写
  • HandleFunc 注册 POST 和 GET 路由

整个过程不到 30 秒,生成的代码直接编译通过:

go build -o pastebin.exe main.go

关键要点:需求描述越具体,生成代码越精准。明确说了"标准库、无第三方依赖",OpenCode 就避免了引入外部路由库。

步骤二:添加 Web 界面

纯 API 交互虽然能用,但对用户不太友好。我接着对 OpenCode 说:

> 给首页加一个 Web 表单,用暗色主题。textarea 输入文本,select 下拉选择代码语言。查看页面也用 HTML 展示,保留"新建 Paste"链接跳回首页。

OpenCode 分析了现有代码,在 main.go 里直接嵌入了两套 HTML 模板——一套 indexTpl 用于首页表单,一套 viewTpl 用于查看页面。CSS 以 <style> 内联方式写在模板里,省去了额外的静态文件。

生成的首页是一个深色背景的简洁表单:顶部标题、居中文本框、语言下拉菜单和提交按钮。查看页面用 <pre> 标签展示预格式化文本,并显示语言类型标签。

从发出指令到看到带 UI 的页面,不超过 1 分钟。

步骤三:打磨细节

基础功能跑通后,还有几个细节需要打磨。我继续对 OpenCode 说:

> 1. 空内容提交时不要创建记录,直接回到首页
> 2. 把 Go 模板字符串的缩进格式化一下,方便阅读
> 3. 启动时打印访问地址

OpenCode 逐项处理:

  • 在 POST 处理分支中,用 strings.TrimSpace 清理输入后判断是否为空
  • 重新排版了嵌入模板的缩进
  • ListenAndServe 前加了 fmt.Println 打印服务地址

每次改动都精准命中,没有引入新 bug。这就是 AI 编程助手的优势——上下文连贯,改动不会偏离主线

步骤四:运行测试

我对 OpenCode 说:

> 启动服务,用 curl 测试一下 POST 和 GET 是否正常

OpenCode 启动了 go run main.go,然后自动构造 curl 命令测试:

# 创建 paste
curl -X POST -d "content=package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}" http://localhost:8080/

# 查看 paste(跟随重定向)
curl -L http://localhost:8080/ -d "content=hello world&lang=go"

随后又用浏览器风格的 curl 请求 GET 链接,验证返回了带有代码内容的 HTML 页面,状态码 200。整个测试流程由 OpenCode 自动完成,无需手动翻阅 curl 参数。

完整代码

package main

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"html/template"
	"net/http"
	"strings"
	"sync"
)

type Paste struct {
	ID      string
	Content string
	Lang    string
}

var (
	store = make(map[string]Paste)
	mu    sync.RWMutex
)

func genID() string {
	b := make([]byte, 4)
	rand.Read(b)
	return hex.EncodeToString(b)
}

var indexTpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Pastebin</title>
<style>
body{max-width:800px;margin:40px auto;font-family:monospace;padding:0 20px}
textarea{width:100%;height:200px;margin:10px 0;padding:10px;background:#1e1e1e;color:#d4d4d4;border:1px solid #333}
select,input[type=submit]{padding:8px 16px;margin:5px 5px 5px 0}
pre{background:#1e1e1e;color:#d4d4d4;padding:16px;overflow-x:auto;border-radius:4px}
a{color:#4ec9b0}
</style></head><body>
<h1>Pastebin</h1>
<form method="POST" action="/">
<textarea name="content" placeholder="粘贴你的代码或文本..."></textarea><br>
<select name="lang">
<option value="">纯文本</option>
<option value="go">Go</option>
<option value="python">Python</option>
<option value="js">JavaScript</option>
<option value="rust">Rust</option>
</select>
<input type="submit" value="创建 Paste">
</form>
</body></html>`))

var viewTpl = template.Must(template.New("view").Parse(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>{{.ID}} - Pastebin</title>
<style>
body{max-width:800px;margin:40px auto;font-family:monospace;padding:0 20px}
pre{background:#1e1e1e;color:#d4d4d4;padding:16px;overflow-x:auto;border-radius:4px}
a{color:#4ec9b0}
</style></head><body>
<h3>Paste #{{.ID}} {{if .Lang}}({{.Lang}}){{end}}</h3>
<pre>{{.Content}}</pre>
<a href="/">新建 Paste</a>
</body></html>`))

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			r.ParseForm()
			content := strings.TrimSpace(r.FormValue("content"))
			if content == "" {
				http.Redirect(w, r, "/", 302)
				return
			}
			id := genID()
			mu.Lock()
			store[id] = Paste{ID: id, Content: content, Lang: r.FormValue("lang")}
			mu.Unlock()
			http.Redirect(w, r, "/"+id, 302)
			return
		}
		path := strings.TrimPrefix(r.URL.Path, "/")
		if path == "" {
			indexTpl.Execute(w, nil)
			return
		}
		mu.RLock()
		p, ok := store[path]
		mu.RUnlock()
		if !ok {
			http.NotFound(w, r)
			return
		}
		viewTpl.Execute(w, p)
	})

	fmt.Println("Pastebin 服务运行在 http://localhost:8080")
	http.ListenAndServe(":8080", nil)
}

小结

这 4 分钟的开发体验和传统方式有几个明显差异:

  • 零次查文档:不需要翻 net/http 或 html/template 的用法,OpenCode 直接给出正确代码
  • 零次调试:代码生成后直接编译通过,测试一遍过
  • 迭代式对话:像和同事 pair programming 一样,用自然语言逐步完善功能
  • 上下文连贯:每一轮对话都基于已有代码,改动不会偏离主线

整个项目 82 行代码,从构思到能运行的成品,4 分钟一气呵成。Pastebin 这类小工具在日常开发中需求频繁,但手动写又嫌麻烦——这正是 OpenCode 这类 AI 编程助手的理想应用场景。当工具性编码可以交由 AI 完成,开发者就能把精力集中在更复杂的架构设计和业务逻辑上。