mcpField
G
go

go

github.com/maragudk开发4945 次安装31 周前更新
代码仓库

关于 go

面向 Go 应用和模块开发的指南,供遵循作者约定的编码 Agent 使用。内容涵盖项目结构、依赖注入、测试、代码检查和文档规范。

主要功能

  1. 01应用与包的结构
  2. 02依赖注入约定
  3. 03使用真实依赖测试
  4. 04测试与代码检查命令
  5. 05Go 文档规范

适用场景

  1. 01规划 Go Web 应用结构
  2. 02编写数据库相关的包测试
  3. 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-pointer CSS 类
  • 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。

评论

支持 Markdown