跳转到主内容
极星编程网:以代码为星,赴技术山海!

Golang如何封装公共工具包_新手项目代码结构设计

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。 真正的复杂点不在目录名,而在每个函数是否清楚自己「能做什么、不能做什么、依赖谁、被谁依赖」——工具包越早建立这种边界感,后期重构成本越低。

相关文章