Go项目中公共工具包应放在pkg/目录下,按功能拆分为pkg/httpx、pkg/strutil等子包;internal/仅用于仅限本项目内部使用的模块,如internal/handler。
公共工具包该放在项目哪个目录
Go 项目里没有强制的“utils”目录规范,但社区普遍接受
复制 internal/pkg
或
复制 pkg
作为公共工具包根路径。不建议放
复制 internal/utils
——
复制 internal
下的包默认禁止被外部项目导入,而你封装的工具包很可能需要被本项目多个模块复用(比如
复制 cmd
和
复制 api
),放这里会导致循环依赖或不可见。
更稳妥的选择是:
复制 pkg/
(导出友好)+ 按功能拆分子包,例如:
复制 pkg/httpx
:封装 HTTP 客户端、重试、超时、日志埋点
复制 pkg/strutil
:字符串安全截断、模糊匹配、正则提取
复制 pkg/errutil
:错误包装、分类判断(如
复制 errors.Is(err, ErrNotFound)
)
避免把所有函数塞进一个
复制 pkg/utils.go
——Go 不支持跨包重载,命名冲突和测试隔离会很快变成噩梦。
如何设计可测试、不依赖全局状态的工具函数
新手常写带全局配置的工具函数,比如:
立即学习
“
go语言免费学习笔记(深入)
”;
复制 var defaultTimeout = 5 * time.Second
func DoRequest(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// ...
}
这类函数看似简洁,实际无法控制超时、无法 mock、无法并行测试。正确做法是显式传参 + 提供配置选项:
基础函数只接收必要参数,不读
环境变量
、不读全局变量
用函数选项模式(Functional Options)封装可选行为,例如
复制 WithTimeout(10*time.Second)
返回值包含错误,不 panic(除非是开发期断言,如
复制 assert.NotNil(t, x)
)
示例(
复制 pkg/httpx/client.go
):
复制 type ClientOption func(*Client)
func WithTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.timeout = d
}
}
type Client struct {
timeout time.Duration
client *http.Client
}
func NewClient(opts ...ClientOption) Client {
c := &Client{timeout: 5 time.Second}
for _, opt := range opts {
opt(c)
}
c.client = &http.Client{Timeout: c.timeout}
return c
}
什么时候该用
复制 internal/
而不是
复制 pkg/
答案很直接:当某段逻辑**只服务于本项目内部模块,且明确不希望被其他项目 import** 时,才放进
复制 internal/
。
MySQL(Linux)
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
下载
复制 internal/handler
:HTTP
路由
处理,强耦合本项目路由框架和中间件
复制 internal/storage
:封装了本项目专用的 Redis 分片策略或 MySQL 分表逻辑
复制 internal/config
:解析
复制 config.yaml
并注入到各模块,结构体含私有字段
反例:把
复制 internal/uuid
封装成通用 UUID 生成器——它不依赖项目上下文,应该进
复制 pkg/uuid
,否则别人想复用还得 copy-paste。
Go 编译器会在构建时检查
复制 internal/
的导入路径合法性,一旦违规会报错:
复制 import "xxx/internal/yyy" is not allowed to refer to package "xxx/internal/yyy"
—— 这个限制是保护机制,不是障碍。
新手最容易忽略的初始化与副作用问题
工具包里藏 init() 函数、全局变量赋值、log.SetOutput()、pprof 注册,是线上事故高发区。这些操作在包被任意导入时就触发,顺序不可控。
绝对不要在
复制 pkg/
下任何文件里写
复制 init()
日志配置、指标注册、信号监听等,统一收口到
复制 cmd/root.go
的 main 入口处
如果某个工具函数必须初始化(比如连接池),提供
复制 NewXXX()
构造函数,并让调用方显式调用
比如
复制 pkg/cache/redis.go
应该这样设计:
复制 type RedisCache struct {
client *redis.Client
}
func NewRedisCache(addr string, password string) (*RedisCache, error) {
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
})
if err := client.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("failed to connect redis: %w", err)
}
return &RedisCache{client: client}, nil
}
而不是在包级变量里直接 new 并 ping。
真正的复杂点不在目录名,而在每个函数是否清楚自己「能做什么、不能做什么、依赖谁、被谁依赖」——工具包越早建立这种边界感,后期重构成本越低。