很多前端开发者习惯用 npx serve 快速启动一个静态文件服务器。今天我们用 Go 从零写一个命令行 HTTP 文件服务器,功能更丰富:静态文件服务、美观的目录列表、浏览器端文件上传,只用标准库,不依赖任何第三方包。
技术栈:Go(net/http + html/template),最终代码约 110 行。
新建项目目录 fileserver,用 OpenCode 打开这个目录,开始对话。
我先给 OpenCode 描述清楚需求——项目结构、CLI 参数、核心功能点一次说完,避免反复澄清。
我对 OpenCode 说:
> 帮我用 Go 写一个命令行 HTTP 文件服务器,要求:
> 1. 文件名 main.go,只用标准库
> 2. 支持 -p 参数指定端口(默认 8080),-d 参数指定根目录(默认当前目录 .)
> 3. 访问 / 显示目录列表,点击文件名可以下载/浏览
> 4. 目录列表隐藏 . 开头的隐藏文件
> 5. 文件大小用 KB/MB 等人类可读格式显示
> 6. 目录列表页面要美观,包含内联 CSS 样式
> 7. 页面上方提供一个文件上传表单,上传到根目录
> 8. 代码控制在 120 行以内
OpenCode 响应:
OpenCode 直接生成了完整的 main.go,包含所有功能——CLI 参数解析、文件服务路由、目录列表 HTML 模板、文件上传处理、文件大小格式化。一次到位。
关键要点:把需求说清楚、说完整,OpenCode 一次就能给出可用代码,不需要来回多轮对话。
生成代码后,我要求 OpenCode 补充上传功能。
我对 OpenCode 说:
> 在目录列表页面上方加一个文件上传表单,POST 到 /upload,上传成功刷新目录。
OpenCode 响应:
OpenCode 在 HTML 模板中插入了 <form> 表单,并在 main() 中注册了 /upload 路由,用 handleUpload 函数接收文件流并写入磁盘。改动精准,未影响其他逻辑。
关键要点:增量修改用简洁的指令,OpenCode 会精准地在现有代码中插入新功能,不会破坏已有逻辑。
默认的目录列表太丑了,我需要一张带 CSS 样式的页面。
我对 OpenCode 说:
> 美化目录列表页面,用等宽字体、简洁配色,表格展示文件名和大小,上方显示当前路径。
OpenCode 响应:
OpenCode 修改了 HTML 模板,加入了完整的内联 CSS——等宽字体、蓝色链接、灰色文件大小文字、分隔线、上传表单。效果类似 GitHub 的目录浏览页面。
关键要点:把"美"描述成具体的设计要求(字体、配色、布局),OpenCode 才能生成你想要的样式。
写完代码,让 OpenCode 自己跑一下验证功能。
我对 OpenCode 说:
> 帮我编译并运行这个服务器,测试一下能不能正常访问。
OpenCode 响应:
go run main.go -p 9999 -d .
OpenCode 启动了服务器,并用 curl 验证了三个场景:
GET / 返回 200,页面包含 HTML 目录列表GET /main.go 返回 200,正确下载源文件POST /upload 上传测试文件成功全部通过,代码开箱即用。
关键要点:让 OpenCode 自己验证自己的代码,它能看到运行日志、检查返回内容,比人工验证更快。
经过上面 4 轮对话,一个功能完备的 HTTP 文件服务器就完成了——静态文件服务、美观目录列表、浏览器上传,一共约 110 行 Go 代码。
package main
import (
"flag"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
var (
port = flag.String("p", "8080", "监听端口")
rootDir = flag.String("d", ".", "根目录")
)
var dirHTML = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Path}}</title>
<style>
body { font-family: "SF Mono", Menlo, Consolas, monospace; max-width: 860px; margin: 30px auto; padding: 0 20px; color: #24292e; }
h1 { font-size: 18px; padding-bottom: 10px; border-bottom: 1px solid #e1e4e8; }
.upload { margin: 16px 0; padding: 12px 16px; background: #f6f8fa; border-radius: 6px; }
.upload input[type=submit] { margin-left: 8px; padding: 4px 14px; cursor: pointer; }
table { width: 100%; border-collapse: collapse; }
td { padding: 6px 12px; border-bottom: 1px solid #f0f0f0; }
.size { text-align: right; color: #6a737d; white-space: nowrap; }
a { color: #0366d6; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>{{.Path}}</h1>
<div class="upload">
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<input type="submit" value="上传">
</form>
</div>
<table>
{{if .Parent}}<tr><td colspan="2"><a href="{{.Parent}}">../</a></td></tr>{{end}}
{{range .Entries}}
<tr>
<td><a href="{{.URL}}">{{.Name}}</a></td>
<td class="size">{{.Size}}</td>
</tr>
{{end}}
</table>
</body>
</html>`
var tmpl = template.Must(template.New("dir").Parse(dirHTML))
type Entry struct {
Name, URL, Size string
}
type DirData struct {
Path string
Parent string
Entries []Entry
}
func main() {
flag.Parse()
absDir, err := filepath.Abs(*rootDir)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/upload", handleUpload)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
servePath(w, r, absDir)
})
addr := fmt.Sprintf(":%s", *port)
fmt.Printf("服务已启动 → http://localhost%s\n根目录: %s\n", addr, absDir)
log.Fatal(http.ListenAndServe(addr, nil))
}
func servePath(w http.ResponseWriter, r *http.Request, root string) {
upath := filepath.Clean(r.URL.Path)
fpath := filepath.Join(root, upath)
info, err := os.Stat(fpath)
if err != nil {
http.NotFound(w, r)
return
}
if info.IsDir() {
listDir(w, fpath, upath)
} else {
http.ServeFile(w, r, fpath)
}
}
func listDir(w http.ResponseWriter, fpath, upath string) {
entries, _ := os.ReadDir(fpath)
data := DirData{Path: upath}
if upath != "/" {
data.Parent = filepath.Dir(upath)
}
for _, e := range entries {
name := e.Name()
if strings.HasPrefix(name, ".") {
continue
}
info, _ := e.Info()
if e.IsDir() {
name += "/"
data.Entries = append(data.Entries, Entry{Name: name, URL: upath + "/" + e.Name(), Size: "-"})
} else {
data.Entries = append(data.Entries, Entry{
Name: name,
URL: upath + "/" + e.Name(),
Size: humanSize(info.Size()),
})
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
func handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "上传失败: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
dst, err := os.Create(filepath.Join(*rootDir, header.Filename))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
io.Copy(dst, file)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func humanSize(n int64) string {
if n < 1024 {
return fmt.Sprintf("%d B", n)
}
units := []string{"KB", "MB", "GB", "TB"}
var i int
f := float64(n)
for i = 0; f >= 1024 && i < len(units)-1; i++ {
f /= 1024
}
return fmt.Sprintf("%.1f %s", f, units[i])
}
用 OpenCode 写这个项目,从第一句需求到完整可运行,实际只花了约 4 分钟。对比传统手写:手动写 Go 的 net/http 路由、html/template、文件上传处理、CSS 内联样式,即使熟练也要 20-30 分钟。
效率提升的核心在于:
下一步你可以试试:让 OpenCode 给这个文件服务器加上 Basic Auth 认证、或者支持 HTTPS。把需求说清楚,敲回车就行。
> 使用命令:go run main.go -p 8080 -d /path/to/share