Featured image of post Go Module 与工程化项目结构

Go Module 与工程化项目结构

Go Module 完整教程:多模块工作区、replace 指令、工程化目录布局、依赖管理最佳实践

前置知识

  • 已安装 Go 1.11+
  • 跑通过 go run . 跑通 Hello World
  • 知道 import 是什么

为什么需要 Go Module

2018 年之前,Go 项目的依赖管理一直被诟病:

  • 第三方包没有版本概念,go get 永远拉 master
  • 多个项目依赖同一包不同版本就崩
  • 公司内部私有包必须放 $GOPATH/src

Go 1.11(2018-08) 引入 Go Module,通过 go.mod + go.sum 两个文件彻底解决:

  • 语义化版本(v1.2.3
  • 内容哈希校验(防止供应链投毒)
  • 跨项目工作区(多 module 并行开发)
  • 私有仓库支持(GOPRIVATE / GONOSUMDB)

一、Go Module 基础

1.1 初始化

1
2
3
mkdir myproject
cd myproject
go mod init github.com/yourname/myproject

生成 go.mod

1
2
3
module github.com/yourname/myproject

go 1.19

1.2 依赖管理命令

1
2
3
4
5
6
7
8
9
go get github.com/gin-gonic/gin@v1.9.1    # 拉取指定版本
go get -u github.com/gin-gonic/gin         # 升级到最新
go get -u=patch                            # 升级 patch 版本
go mod tidy                                # 清理无用 + 补全缺失
go mod download                            # 下载所有依赖到本地
go mod verify                              # 校验哈希
go mod vendor                              # 复制依赖到 ./vendor/
go mod graph                               # 打印模块依赖图
go mod why github.com/gin-gonic/gin        # 解释为什么需要这个依赖

1.3 go.mod 文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
module github.com/yourname/myproject

go 1.19

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/spf13/viper v1.16.0
)

require github.com/stretchr/testify v1.8.4 // indirect

replace github.com/old/pkg => github.com/new/pkg v1.0.0
  • module:当前模块路径
  • go:最低 Go 版本要求
  • require:直接依赖
  • // indirect:间接依赖(被其他 require 引入)
  • replace:本地或 fork 替换
  • exclude:排除某个版本

1.4 go.sum 是什么

go.sum 记录每个依赖的内容哈希(h1:xxxx):

1
2
github.com/gin-gonic/gin v1.9.1 h1:9WCKbRy+nQzf2 instance=...
github.com/gin-gonic/gin v1.9.1/go.mod h1:7fiKi...

作用:团队协作时,CI 拉下来的依赖如果哈希不一致直接报错——防止中间人篡改。

二、多模块工作区(Go 1.18+)

痛点场景:你在同时开发两个模块 myappmylibmyapp 引用 mylib 的最新代码——mylib 还没发布,myapp 怎么调试?

2.1 旧方案:go.mod replace

1
2
3
// myapp/go.mod
require github.com/yourname/mylib v0.0.0
replace github.com/yourname/mylib => ../mylib

缺点:每次 go mod tidy 都得手动维护 replace,切回线上版本时容易忘。

2.2 新方案:go.work(Go 1.18+)

1
2
3
4
5
6
7
8
mkdir workspace
cd workspace

mkdir myapp && cd myapp && go mod init github.com/yourname/myapp
mkdir ../mylib && cd ../mylib && go mod init github.com/yourname/mylib
cd ..

go work init myapp mylib

生成 go.work

1
2
3
4
5
6
go 1.19

use (
    ./myapp
    ./mylib
)

效果myapp 自动用 ./mylib 的最新代码,不用改 go.modreplace 一行不需要。

go.work 文件不要提交到 Git(加到 .gitignore)——只在本地开发用。

三、典型工程化目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
---------------------------------- go.mod            模块导入
---------------------------------- config            配置文件
---------------------------------- cmd               程序入口(可以有多个)
---------------------------------- ----- 程序1        main 入口
---------------------------------- ----- 程序2        main 入口
---------------------------------- doc               项目文档
---------------------------------- internal          内部包(外部无法 import)
---------------------------------- pkg               共享包(公开)
---------------------------------- public            web 前端
---------------------------------- resources         静态资源
---------------------------------- scripts           脚本(部署、运维)
---------------------------------- swagger           OpenAPI 文档
---------------------------------- test              集成测试

3.1 cmd 目录

每个可执行程序一个子目录:

1
2
3
4
5
cmd/
├── server/
│   └── main.go    # 编译出 bin/server
└── cli/
    └── main.go    # 编译出 bin/cli
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// cmd/server/main.go
package main

import (
    "github.com/yourname/myapp/internal/server"
)

func main() {
    server.Run()
}

3.2 internal 包

internal 目录下的包只能被同父目录的包引用——Go 编译器强制保护:

1
2
3
4
5
6
7
myproject/
├── internal/
│   ├── dao/        # 数据访问
│   ├── logic/      # 业务逻辑
│   └── model/      # 数据模型
├── pkg/            # 公开包
│   └── utils/

外部项目 import "github.com/yourname/myapp/internal/dao" —— 编译失败。

3.3 pkg vs internal

目录可见性用途
pkg/外部可引用通用工具、SDK
internal/仅项目内部业务实现、DAO

3.4 go.mod 完整示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module github.com/yourname/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/spf13/viper v1.16.0
    github.com/go-sql-driver/mysql v1.7.1
    gorm.io/gorm v1.25.5
    go.uber.org/zap v1.26.0
)

require (
    github.com/jinzhu/inflection v1.0.0 // indirect
    github.com/jinzhu/now v1.1.5 // indirect
    // ...
)

四、依赖版本管理最佳实践

4.1 选择版本策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 拉取最新稳定版
go get github.com/gin-gonic/gin@latest

# 拉取指定版本
go get github.com/gin-gonic/gin@v1.9.1

# 拉取 v2 大版本
go get github.com/gin-gonic/gin/v2@v2.0.0

# 升级到最新 minor
go get -u=minor github.com/gin-gonic/gin

# 升级到最新 patch
go get -u=patch github.com/gin-gonic/gin

4.2 升级所有依赖

1
2
3
go get -u ./...           # 升级所有直接依赖
go mod tidy               # 重新整理
go test ./...             # 跑测试确认没炸

4.3 降级依赖

1
2
3
# 锁定到旧版本
go get github.com/gin-gonic/gin@v1.8.0
go mod tidy

4.4 私有仓库配置

1
2
3
4
5
# 设置 GOPRIVATE,所有子路径不走代理
go env -w GOPRIVATE=github.com/yourcompany/*,gitlab.yourcompany.com/*

# 配置 SSH 拉取(推荐)
git config --global url."git@github.com:".insteadOf "https://github.com/"

4.5 镜像配置

1
2
3
4
5
# 国内镜像
go env -w GOPROXY=https://goproxy.cn,direct

# 公司内部代理
go env -w GOPROXY=https://goproxy.yourcompany.com,https://goproxy.cn,direct

五、Go Module 常见问题

5.1 go.mod 文件中的 +incompatible

1
require github.com/old/pkg v1.0.0+incompatible

表示该包未使用 Go Module(没有 go.mod),但有 v1.x.x tag。

5.2 依赖冲突

1
2
3
4
5
6
# 查看冲突
go mod graph | grep gin

# 强制使用某个版本
go get github.com/gin-gonic/gin@v1.9.1
go mod tidy

5.3 vendor 目录

1
2
3
4
5
# 把所有依赖复制到 ./vendor/
go mod vendor

# 编译时优先用 vendor
go build -mod=vendor

适用:离线环境、严格审计场景、Docker 构建不想走网络。

六、CI/CD 集成

6.1 GitHub Actions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
name: build
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.21'
          cache: true
      - run: go mod download
      - run: go test ./...
      - run: go build -o bin/server ./cmd/server

6.2 镜像构建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 多阶段构建
FROM golang:1.21-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/server

FROM alpine:3.18
RUN apk --no-cache add ca-certificates tzdata
COPY --from=build /out/server /usr/local/bin/server
ENTRYPOINT ["server"]

最终镜像 ~15MB,scratch 基础镜像可压到 10MB 以下。

七、下一步

参考资料

使用 Hugo 构建
主题 StackJimmy 设计