关于 go
面向 Go 应用和模块开发的指南,供遵循作者约定的编码 Agent 使用。内容涵盖项目结构、依赖注入、测试、代码检查和文档规范。
主要功能
- 01应用与包的结构
- 02依赖注入约定
- 03使用真实依赖测试
- 04测试与代码检查命令
- 05Go 文档规范
适用场景
- 01规划 Go Web 应用结构
- 02编写数据库相关的包测试
- 03运行项目测试与代码检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ maragudk/ skills --skill go安装后在 Claude Code 中输入/go确认已加载。
详细介绍
Go
本指南介绍如何使用 Go 开发应用程序和模块/库。
其中一些内容仅适用于应用程序,不适用于在其他项目中作为库使用的模块,例如数据库访问和运行服务。
应用程序结构
我通常会构建 Web 应用程序和库/模块。
应用程序中通常会包含以下包(有些可能缺失,这通常表示项目不需要它们)。
main:包含应用程序的主入口点(位于目录cmd/app)model:包含其他包中使用的领域模型sql/sqlite/postgres:包含 SQL 数据库相关逻辑、数据库迁移(位于子目录migrations/)和测试夹具(位于子目录testdata/fixtures/)。使用的数据库为 SQLite 或 PostgreSQL。sqltest/sqlitetest/postgrestest:用于测试的包,用于创建和清理测试数据库s3:用于与 Amazon S3 或兼容的对象存储交互的逻辑s3test:用于测试的包,用于创建和清理测试 S3 存储桶llm:用于与大模型(LLM)和基础模型交互的客户端llmtest:用于测试的包,用于创建测试用的 LLM 客户端http:应用程序的 HTTP 处理程序html:应用程序的 HTML 模板,使用 gomponents 库编写(如需了解其用法,请参阅 https://www.gomponents.com/llms.txt)
代码风格
依赖注入
我会在组件之间大量使用依赖注入。这通常通过接收端的私有接口实现。请注意下面示例中 userGetter 的用法:
package http
import (
"net/http"
"github.com/go-chi/chi/v5"
"maragu.dev/httph"
"model"
)
type UserResponse struct {
Name string
}
type userGetter interface {
GetUser(ctx context.Context, id model.ID) (model.User, error)
}
func User(r chi.Router, db userGetter) {
r.Get("/user", httph.JSONHandler(func(w http.ResponseWriter, r *http.Request, _ any) (UserResponse, error) {
id := r.URL.Query().Get("id")
user, err := db.GetUser(r.Context(), model.ID(id))
if err != nil {
return UserResponse{}, httph.HTTPError{Code: http.StatusInternalServerError, Err: errors.New("error getting user")}
}
return UserResponse{Name: user.Name}, nil
}))
}
测试
我会为大多数函数和方法编写测试。我几乎总是使用子测试,并清晰描述测试内容和预期结果。
示例如下:
package example
type Thing struct {}
func (t *Thing) DoSomething() (bool, error) {
return true, nil
}
package example_test
import (
"testing"
"maragu.dev/is"
"example"
)
func TestThing_DoSomething(t *testing.T) {
t.Run("should do something and return a nil error", func(t *testing.T) {
thing := &example.Thing{}
ok, err := thing.DoSomething()
is.NotError(t, err)
is.True(t, ok)
})
}
有时我会使用表驱动测试:
package example
import "errors"
type Thing struct {}
var ErrChairNotSupported = errors.New("chairs not supported")
func (t *Thing) DoSomething(with string) error {
if with == "chair" {
return ErrChairNotSupported
}
return nil
}
package example_test
import (
"testing"
"maragu.dev/is"
"example"
)
func TestThing_DoSomething(t *testing.T) {
tests := []struct {
name string
input string
expected error
}{
{name: "should do something with the table and return a nil error", input: "table", expected: nil},
{name: "should do something with the chair and return an ErrChairNotSupported", input: "chair", expected: example.ErrChairNotSupported},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
thing := &example.Thing{}
err := thing.DoSomething(test.input)
if test.expected != nil {
is.Error(t, test.expected, err)
} else {
is.NotError(t, err)
}
})
}
}
相比 Mock,我更喜欢使用真实依赖进行集成测试,因为没有什么比真实环境更可靠。依赖通常在 Docker 容器中运行。可以假设运行测试时这些依赖已启动。
如果测试的重点不在于依赖,而是依赖只起次要作用,那么使用 Mock 是合理的。但例如,测试数据库方法时,应使用真实的底层数据库。
我使用 maragu.dev/is 模块中的测试断言。可用函数包括:is.True、is.Equal、is.Nil、is.NotNil、is.EqualSlice、is.NotError、is.Error。这些函数都可以在最后一个参数中接收可选消息。
由于测试会被打乱顺序,不要依赖测试顺序,即使是子测试也一样。
每次调用 postgrestest.NewDatabase(t)/sqlitetest.NewDatabase(t) 测试辅助函数时,数据库都会处于干净状态(没有其他测试留下的数据等)。
可以使用数据库夹具进行测试。当多个测试依赖相同或非常相似的数据时,优先使用夹具来准备测试数据,这样每个测试就不必重复准备相同的数据。夹具位于 sqlite/testdata/fixtures/postgres/testdata/fixtures。使用方式为 sqlitetest.NewDatabase(t, sqlitetest.WithFixtures("fixture one", "fixture two"))。夹具会按给定顺序应用。
测试辅助函数应调用 testing.T.Helper()。
在测试中,使用 t.Context() 而不是 context.Background(),并始终在需要的位置直接使用,不要先赋值给 ctx 变量。
杂项
- 变量命名:
- 请求使用
req,响应使用res
- 请求使用
- 可使用 SQL 辅助函数:
Database.H.Select、Database.H.Exec、Database.H.Get、Database.H.InTx。 - 在 Go 中使用内置类型
any,而非interface{} - 标准库中的
sql.ErrNoRows在maragu.dev/glue/sql.ErrNoRows中有别名,因此无需同时导入两者 - 所有 HTML 按钮都需要添加
cursor-pointerCSS 类 - SQLite 时间格式始终是由
strftime('%Y-%m-%dT%H:%M:%fZ')返回的字符串。使用数据库时,请使用maragu.dev/glue/model.Time(在项目中通常别名为model.Time),而非标准库的time.Time。 - 请记住,Go 中的私有函数属于包级,因此可以在同一包的不同文件中使用
- 文档必须遵循 Go 风格:以标识符名称作为句子的第一个词,然后补全句子,避免重复表达。例如:
// SearchProducts using the given search query and result limit.,而非:// SearchProducts searches products using the given search query and result label. - 默认情况下,包级标识符必须以小写字母开头,即仅在包内可见,以缩小对其他包开放的 API 范围。
- 将任意值转换为字符串时,请使用
fmt.Sprint,而非strconv中的函数。 - 使用
new()(自 Go 1.26 起提供),而非任何用于为字面量创建指针的函数。
测试、Lint 和评估
运行 make test 或 go test -shuffle on ./... 执行所有测试。若要只运行某个包中的测试,请运行 go test -shuffle on ./path/to/package。若要运行特定测试,请运行 go test ./path/to/package -run TestName。
运行 make lint 或 golangci-lint run 执行 Linter。必须始终在包/目录级别运行;对单个文件运行不起作用。
运行 make eval 或 go test -shuffle on -run TestEval ./... 执行大模型评估。
运行 make fmt 格式化项目中的所有代码,适合作为最后的整理步骤。
你可以在 Shell 中使用 psql 或 sqlite3 访问数据库。
文档
通常可以使用 go doc 和模块名称查询 Go 模块的文档。例如,查询标准库中的内容可以运行 go doc net/http,查询第三方模块可以运行 go doc maragu.dev/gai。你也可以使用类似 go doc maragu.dev/gai.ChatCompleter 的命令,查询特定标识符的更详细文档;这里查询的是 ChatCompleter 接口。
在浏览器中检查应用
你可以假设应用正在运行,并可通过 Chrome Dev Tools MCP 工具在浏览器中访问。代码变更会自动重新加载,因此无需手动刷新。
运行中应用的日志输出位于项目根目录下的 app.log。