| name | golang-dev |
| description | Go 开发规范与最佳实践,涵盖项目结构、错误处理、并发模式、接口设计、Web 框架、单元测试 |
| version | 1.0 |
Go 开发规范
概述
本 Skill 指导 Go 项目的开发规范与最佳实践,包括项目结构、错误处理、并发模式、接口设计、Web 框架使用和测试。
适用场景:
- 新建 Go 微服务项目
- 设计并发安全的业务逻辑
- gin/echo Web 框架最佳实践
- 编写 table-driven 单元测试
- 错误处理与 wrap 策略
项目结构
标准布局(适合中大型服务)
my-service/
├── cmd/ # 入口程序
│ └── server/
│ └── main.go # main 函数,只做组装和启动
├── internal/ # 私有代码(外部不可导入)
│ ├── config/ # 配置加载
│ │ └── config.go
│ ├── domain/ # 领域层(核心业务逻辑)
│ │ ├── model/ # 领域模型
│ │ │ └── user.go
│ │ ├── repository/ # 仓储接口
│ │ │ └── user.go
│ │ └── service/ # 领域服务
│ │ └── user.go
│ ├── handler/ # HTTP 处理器(接口层)
│ │ └── user.go
│ ├── middleware/ # 中间件
│ │ ├── auth.go
│ │ └── logger.go
│ └── infra/ # 基础设施(数据库、缓存等实现)
│ ├── mysql/
│ │ └── user_repo.go
│ └── redis/
│ └── cache.go
├── pkg/ # 可导出的公共库
│ ├── errcode/ # 错误码定义
│ │ └── errcode.go
│ └── response/ # 统一响应
│ └── response.go
├── api/ # API 定义(OpenAPI、protobuf)
│ └── openapi.yaml
├── scripts/ # 构建、部署脚本
├── go.mod
├── go.sum
└── Makefile
关键原则
package main
import (
"context"
"log"
"my-service/internal/config"
"my-service/internal/handler"
"my-service/internal/infra/mysql"
"my-service/internal/domain/service"
)
func main() {
cfg := config.MustLoad()
db := mysql.MustConnect(cfg.Database)
defer db.Close()
userRepo := mysql.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
router := setupRouter(userHandler)
if err := router.Run(cfg.Server.Addr); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
错误处理
基本原则
result, err := doSomething()
if err != nil {
return fmt.Errorf("执行操作失败: %w", err)
}
if err != nil {
log.Error("失败", err)
return err
}
sentinel errors 与自定义错误
package errcode
import "errors"
var (
ErrNotFound = errors.New("资源不存在")
ErrUnauthorized = errors.New("未授权")
ErrForbidden = errors.New("无权限")
ErrDuplicate = errors.New("资源已存在")
)
type BizError struct {
Code string
Message string
Err error
}
func (e *BizError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *BizError) Unwrap() error {
return e.Err
}
func NewBizError(code, message string) *BizError {
return &BizError{Code: code, Message: message}
}
func WrapBizError(code, message string, err error) *BizError {
return &BizError{Code: code, Message: message, Err: err}
}
errors.Is 与 errors.As
if errors.Is(err, errcode.ErrNotFound) {
c.JSON(http.StatusNotFound, response.Fail("资源不存在"))
return
}
var bizErr *errcode.BizError
if errors.As(err, &bizErr) {
c.JSON(http.StatusBadRequest, response.FailWithCode(bizErr.Code, bizErr.Message))
return
}
log.Errorf("未处理错误: %+v", err)
c.JSON(http.StatusInternalServerError, response.Fail("服务器内部错误"))
并发模式
goroutine + WaitGroup(并行执行,等待全部完成)
func fetchAllUserData(ctx context.Context, userID int64) (*UserFullData, error) {
var (
profile *Profile
orders []*Order
balance *Balance
mu sync.Mutex
wg sync.WaitGroup
firstErr error
)
wg.Add(3)
go func() {
defer wg.Done()
p, err := fetchProfile(ctx, userID)
mu.Lock()
defer mu.Unlock()
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("获取用户资料失败: %w", err)
return
}
profile = p
}()
go func() {
defer wg.Done()
o, err := fetchOrders(ctx, userID)
mu.Lock()
defer mu.Unlock()
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("获取订单列表失败: %w", err)
return
}
orders = o
}()
go func() {
defer wg.Done()
b, err := fetchBalance(ctx, userID)
mu.Lock()
defer mu.Unlock()
if err != nil && firstErr == nil {
firstErr = fmt.Errorf("获取余额失败: %w", err)
return
}
balance = b
}()
wg.Wait()
firstErr != {
, firstErr
}
&UserFullData{Profile: profile, Orders: orders, Balance: balance},
}
errgroup(推荐,更优雅的并发错误处理)
import "golang.org/x/sync/errgroup"
func fetchAllUserData(ctx context.Context, userID int64) (*UserFullData, error) {
var (
profile *Profile
orders []*Order
balance *Balance
)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
var err error
profile, err = fetchProfile(ctx, userID)
return err
})
g.Go(func() error {
var err error
orders, err = fetchOrders(ctx, userID)
return err
})
g.Go(func() error {
var err error
balance, err = fetchBalance(ctx, userID)
return err
})
if err := g.Wait(); err != nil {
return nil, fmt.Errorf("获取用户完整数据失败: %w", err)
}
return &UserFullData{Profile: profile, Orders: orders, Balance: balance}, nil
}
channel 模式(生产者-消费者)
func processOrders(ctx context.Context, orderIDs []int64) error {
const workerCount = 5
jobs := make(chan int64, len(orderIDs))
errs := make(chan error, workerCount)
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for orderID := range jobs {
if err := processOneOrder(ctx, orderID); err != nil {
errs <- fmt.Errorf("处理订单 %d 失败: %w", orderID, err)
return
}
}
}()
}
for _, id := range orderIDs {
jobs <- id
}
close(jobs)
go func() {
wg.Wait()
close(errs)
}()
for err := range errs {
return err
}
}
context 超时与取消
func callExternalService(ctx context.Context, req *Request) (*Response, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resultCh := make(chan *Response, 1)
errCh := make(chan error, 1)
go func() {
resp, err := doHTTPCall(ctx, req)
if err != nil {
errCh <- err
return
}
resultCh <- resp
}()
select {
case <-ctx.Done():
return nil, fmt.Errorf("调用外部服务超时: %w", ctx.Err())
case err := <-errCh:
return nil, err
case resp := <-resultCh:
return resp, nil
}
}
接口设计
小接口原则
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
type UserReader interface {
FindByID(ctx context.Context, id int64) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
}
type UserWriter interface {
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
}
type UserRepository interface {
UserReader
UserWriter
}
面向接口编程 — 接口在消费方定义
type UserRepo interface {
FindByID(ctx context.Context, id int64) (*model.User, error)
Save(ctx context.Context, user *model.User) error
}
type UserService struct {
repo UserRepo
}
func NewUserService(repo UserRepo) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(ctx context.Context, id int64) (*model.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("获取用户失败: %w", err)
}
if user == nil {
return nil, errcode.ErrNotFound
}
return user, nil
}
gin Web 框架最佳实践
路由与 Handler
type UserHandler struct {
userSvc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{userSvc: svc}
}
func (h *UserHandler) RegisterRoutes(r *gin.RouterGroup) {
users := r.Group("/users")
{
users.POST("", h.Create)
users.GET("/:id", h.GetByID)
users.PUT("/:id", h.Update)
users.DELETE("/:id", h.Delete)
}
}
func (h *UserHandler) Create(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数格式错误")
return
}
if err := req.Validate(); err != nil {
response.BadRequest(c, err.Error())
return
}
user, err := h.userSvc.CreateUser(c.Request.Context(), &req)
if err != nil {
handleError(c, err)
return
}
response.Created(c, toUserVO(user))
}
统一响应格式
package response
import "github.com/gin-gonic/gin"
type Result struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Result{Code: 0, Message: "success", Data: data})
}
func Created(c *gin.Context, data interface{}) {
c.JSON(http.StatusCreated, Result{Code: 0, Message: "success", Data: data})
}
func BadRequest(c *gin.Context, msg string) {
c.JSON(http.StatusBadRequest, Result{Code: 400, Message: msg})
}
func Fail(msg string) Result {
return Result{Code: -1, Message: msg}
}
func FailWithCode(code string, msg string) Result {
return Result{Code: -1, Message: msg, Data: map[]{: code}}
}
中间件
func RequestLogger(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
logger.Info("请求处理完成",
slog.String("method", c.Request.Method),
slog.String("path", path),
slog.Int("status", c.Writer.Status()),
slog.Duration("latency", time.Since(start)),
slog.String("client_ip", c.ClientIP()),
)
}
}
func JWTAuth(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
response.Unauthorized(c, "缺少认证令牌")
c.Abort()
return
}
claims, err := parseJWT(token, secret)
if err != nil {
response.Unauthorized(c, "认证令牌无效")
c.Abort()
return
}
c.Set("user_id", claims.UserID)
c.Next()
}
}
单元测试(table-driven tests)
基本模式
func TestUserService_GetUser(t *testing.T) {
tests := []struct {
name string
userID int64
mockSetup func(*MockUserRepo)
want *model.User
wantErr error
}{
{
name: "正常获取用户",
userID: 1,
mockSetup: func(m *MockUserRepo) {
m.On("FindByID", mock.Anything, int64(1)).
Return(&model.User{ID: 1, Name: "张三"}, nil)
},
want: &model.User{ID: 1, Name: "张三"},
wantErr: nil,
},
{
name: "用户不存在",
userID: 999,
mockSetup: func(m *MockUserRepo) {
m.On("FindByID", mock.Anything, int64(999)).
Return(nil, nil)
},
want: nil,
wantErr: errcode.ErrNotFound,
},
{
name: "数据库错误",
userID: 1,
mockSetup: func {
m.On(, mock.Anything, ()).
Return(, errors.New())
},
want: ,
wantErr: errors.New(),
},
}
_, tt := tests {
t.Run(tt.name, {
mockRepo := (MockUserRepo)
tt.mockSetup(mockRepo)
svc := service.NewUserService(mockRepo)
got, err := svc.GetUser(context.Background(), tt.userID)
tt.wantErr != {
assert.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr.Error())
assert.Nil(t, got)
} {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
mockRepo.AssertExpectations(t)
})
}
}
HTTP Handler 测试
func TestUserHandler_Create(t *testing.T) {
tests := []struct {
name string
body string
mockSetup func(*MockUserService)
wantStatus int
wantCode int
}{
{
name: "创建成功",
body: `{"username":"zhangsan","email":"zhang@example.com"}`,
mockSetup: func(m *MockUserService) {
m.On("CreateUser", mock.Anything, mock.Anything).
Return(&model.User{ID: 1, Name: "zhangsan"}, nil)
},
wantStatus: http.StatusCreated,
wantCode: 0,
},
{
name: "参数格式错误",
body: `invalid json`,
mockSetup: func(m *MockUserService) {},
wantStatus: http.StatusBadRequest,
wantCode: 400,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSvc := new(MockUserService)
tt.mockSetup(mockSvc)
h := handler.NewUserHandler(mockSvc)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", ,
strings.NewReader(tt.body))
c.Request.Header.Set(, )
h.Create(c)
assert.Equal(t, tt.wantStatus, w.Code)
resp response.Result
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(t, err)
assert.Equal(t, tt.wantCode, resp.Code)
})
}
}
测试辅助工具
import "github.com/stretchr/testify/assert"
import "github.com/stretchr/testify/require"
import "github.com/testcontainers/testcontainers-go"
func setupMySQL(t *testing.T) *sql.DB {
t.Helper()
ctx := context.Background()
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "mysql:8.0",
ExposedPorts: []string{"3306/tcp"},
Env: map[string]string{
"MYSQL_ROOT_PASSWORD": "test",
"MYSQL_DATABASE": "testdb",
},
},
Started: true,
})
require.NoError(t, err)
t.Cleanup(func() { container.Terminate(ctx) })
...
}
编码规范速查
| 规则 | 说明 |
|---|
| 命名 | 包名小写单词、变量 camelCase、导出类型 PascalCase、接口不加 I 前缀 |
| 错误处理 | 永远检查 error,用 %w 包装,不要 _ 忽略 |
| context | 第一个参数始终是 ctx context.Context,不要存储到 struct |
| goroutine | 必须可控退出(通过 context 或 done channel),防止泄漏 |
| 接口 | 在消费方定义,保持小接口(1-3 个方法) |
| 零值可用 | 设计 struct 使零值有合理的默认行为 |
| defer | 资源释放用 defer,确保 Close/Unlock 不遗漏 |
| 日志 | 使用 slog(Go 1.21+),结构化日志替代 fmt.Printf |
| lint | 使用 golangci-lint,配置 .golangci.yml |