internal目录是go编译器强制的私有边界,仅允许同模块内导入,防止前台系统绕过中台鉴权等核心逻辑;service/handler必须放internal下,不可置于pkg或根目录。

internal 目录必须存在,且所有业务核心代码必须放进去——这是 Go 企业中台项目能否真正隔离、可维护、防误引用的第一道硬边界。
为什么不能把 service 或 handler 放在根目录或 pkg 下
Go 的 internal 机制是编译期强制约束:任何位于 internal/ 子路径下的包,**外部模块(包括同项目的其他非 internal 包)无法 import**。中台项目常需对接多个前台系统(如电商、CRM、IoT 平台),若把用户服务逻辑放在 pkg/user,前台项目可能直接 import 并绕过中台统一鉴权、限流、审计逻辑,造成能力泄露和安全缺口。
常见错误现象:import "myproject/pkg/user" works locally but fails in CI with "use of internal package not allowed" —— 实际是因为本地没开 module 模式或 GOPATH 混用,掩盖了问题。
-
cmd/只保留main.go,只做依赖组装和启动,不写任何业务逻辑 -
pkg/仅放真正可复用、无业务上下文的工具,比如pkg/logger、pkg/metrics、pkg/trace;绝不能放pkg/order或pkg/payment - 中台能力暴露必须通过明确的接口层,例如
internal/payment/api提供 gRPC/HTTP 接口定义,internal/payment/service实现,两者之间用 interface 耦合
go mod 初始化与多中台模块共存的关键配置
中台不是单体项目,而是由若干能力模块(如支付、风控、用户中心)组成的集合体。每个能力模块应是独立的 Go module,而非子目录:
- 不要:
go mod init myorg/center然后把 payment、risk 全塞进internal/下——这会导致版本无法单独升级、依赖污染 - 要:
go mod init myorg/payment v1.0.0和go mod init myorg/risk v1.2.0,各自有独立go.mod和go.sum - 主协调项目(如
myorg/center-gateway)通过require显式引入各模块,版本号按语义化严格锁定
这样做的性能与兼容性影响:构建时 Go 不会重复下载同一模块不同版本;CI 可对单个模块(如 payment)跑独立测试流水线;上线灰度时可只发布 payment@v1.0.1,不影响 risk 模块。
中台 API 层必须做两件事:协议抽象 + 错误标准化
中台对外提供能力,不是暴露 Go 函数,而是定义清晰、稳定、跨语言可理解的契约。这意味着:
- HTTP 层不用
net/http原生 handler,改用gorilla/mux或gin统一路由+中间件链,但关键点在于:所有 handler 必须调用service层返回的 error interface,不能自己http.Error到处写 - gRPC 是更推荐的选择:用
api/payment/v1/payment.proto定义方法,gqlgen或protoc-gen-go生成强类型 stub,天然隔离实现细节 - 错误必须统一为中台标准码,例如:
ErrCodePaymentTimeout = "PAYMENT_TIMEOUT",而不是直接抛context.DeadlineExceeded;前端或调用方靠这个字符串做重试/降级,不是靠 Go 类型
容易踩的坑:在 handler 里用 fmt.Errorf("failed to create order: %w", err) 包裹底层 error,结果日志里全是嵌套堆栈,但调用方收不到结构化错误码——中台的错误不是给开发者看的,是给调用系统决策用的。
配置与环境隔离必须靠 viper + 分层覆盖,不能靠 if-else
中台要同时支撑开发、预发、生产、甚至客户私有化部署,配置差异极大。硬编码或靠 if cfg.Env == "prod" 切分支,会导致:
- 测试难:无法在本地模拟客户 A 的 Redis 地址 + 客户 B 的证书路径
- 发布风险高:改错一个
if分支,整个中台不可用 - 审计缺失:谁在什么时候改了哪个环境的配置?无迹可查
正确做法是用 viper 按优先级加载:
- 最高:命令行参数(
--redis.addr=...,用于临时调试) - 高:环境变量(
PAYMENT_REDIS_ADDR,K8s ConfigMap 注入) - 中:配置文件(
config/payment.yaml,GitOps 管理) - 最低:代码内默认值(
viper.SetDefault("redis.timeout", 5))
最关键的是:所有配置项必须在 internal/config 包里集中声明并校验,比如 Validate() error 方法检查 redis.addr 非空、timeout > 0,启动时报错退出,而不是运行时 panic。
internal 封死」和「哪些错误码必须被所有模块对齐」——这两件事一旦松动,中台就退化成一堆拼凑的库。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











