要是写Go只是奔着能跑就行去做,那么有着三个月经验的程序员也是能够达成的。然而在生产环境当中,代码的维护成本,排查问题所需的效率,团队协作的顺畅程度,这些才是真正致使差距出现的所在之处啦。下面的这10条实战经验,涵盖了从代码格式一直到分布式追踪的全长链路,每一条都是大团队通过真金白银换来切实踩出来存在的教训哟。
工具链:第一道防线格式化与 Lint
协作项目之中,代码风格保持一致甚是关键。要是每个开发者都运用自身惯用的格式,代码审查便会演变成没完没了的括号之争。Go自带工具 gofmt 以及 goimports,务必在提交之前强制运行,将格式方面的争论完全杜绝。
Mock生成属于另外一个让人为难的点,不少人依靠手动方式去编写mock,一旦接口产生更改就会遭遇崩溃状况,提议采用go:generate指令来进行自动生成mock,于接口文件当中添加//go:generate mockgen -source=xxx.go -destination=mock/mock_xxx.go,然后于根目录搁置一个generate.go,从而使得大家能够通过一键操作维持接口与mock处于同步状态。
代码组织:结构决定清晰度
强烈建议仓库结构遵循社区所公认的 Standard Go Project Layout,新同学克隆下来便能够快速上手,清楚 cmd 放置可执行文件,pkg 放置可导出库,internal 放置私有包,api 放置协议定义,不要自行发明类似轮子般的目录结构,以免浪费时间并且增加沟通成本。
导入语句得进行分组并且要有顺序,理应依据标准库、第三方库、公司内部库、本项目内部包这四层来排列. 乱序的imports在代码审查之际相当碍眼, 并且极易引发循环依赖. 空行分隔得清晰, 一眼就能晓得依赖了哪些外部事物。
import (
"context"
"github.com/sirupsen/logrus"
"github.company.com/org/repo/gapi"
"time"
gdocs "google.golang.org/api/docs/v1"
)
命名:简单不冗余
import (
"context"
"time"
"github.com/sirupsen/logrus"
gdocs "google.golang.org/api/docs/v1"
"github.company.com/org/repo/gapi"
"github.company.com/org/repo/snow"
"github.company.com/org/repo/internal/clients/slack"
)
若包名已然给出了上下文,那就别在类型名当中再进行重复。举例来说,如果包名叫 user,在其中结构体名叫 UserModel 便是冗余之举,直接称作 Model 或者索性就叫 User 反倒更为清晰。于 user 包里引用 user.User 已然颇为明确,非要叫 user.UserModel 那就是在给自己制造麻烦。
“Manager”、“Util”、“Helper”这样的词,于此处而言,纯粹属于噪音范畴,白白增添了阅读时的负担。一旦瞧见这般命名,常常就意味着该类或者函数在职责方面并不明晰,无论何物都能够往里面塞。命名需要精准地对职责予以描述,而绝非借助模糊的词汇来遮掩设计方面存在的问题。
上下文:传参不要塞结构体
type DeploymentTransformerHandlerIntf interface{}
type DeploymentTransformerHandler struct{}
永远将 context.Context 当作函数的首个参数传进去,而不是放置到结构体字段当中。如此这般才能够正确地传播取消信号以及超时。把 context 存储到结构体里,会致使生命周期变得混乱,一次请求超时有可能污染整个对象。
type DeploymentTransformerIntf interface{}
type DeploymentTransformer struct{}
这属于 Go 社区所遵循的硬性方面约定,一旦方法存在对 context 的需求,那么都要放置在首个参数那里。在代码审查期间要是见到 ctx 隐匿于结构体内部,大体上能够断定这是由初学者编写的,必定要打回去进行修改。context 是专门用以传递请求级数据的,并非用于充当对象属性的。
方法签名:拥抱函数选项模式
单是参数数量多起来便会成为灾难,就像那种 NewServer(addr string, port int, timeout time.Duration, maxConns int, tlsConfig tls.Config) 这般的函数示例,当进行调用的时刻,又有谁能够清楚知晓 time.Second30 以及 120 这些数值各自究竟代表着什么含义呢?其具备的可读性糟糕透顶,并且一旦遇上需要增添参数的情况,所有的调用方都不得不做出修改。
type Client struct {
ctx context.Context
}
func (c *Client) DoSomething(foo string) {
// 用 c.ctx —— 千万别这样!
}
推荐采用函数选项模式,具备可读性强且灵活的特性,调用之时书写为 WithTimeout(30*time.Second)、WithMaxConns(120),其中意图清晰可见,后续增添参数也不会对已有调用造成破坏,扩展性十分强大,尽管会多编写几行代码,然而从长期维护角度来看收益颇为巨大。
func (c *Client) DoSomething(ctx context.Context, foo string) {
// 用 ctx
}
错误处理:上下文才是王道
最佳实践是采用堆栈加错误包装来处理错误,推荐运用github.com/pkg/errors或类似的库,为错误自动附上调用栈,手动在字符串里书写“在xxx方法里出错”,维护成本极高并且容易遗漏,更改一个函数名就需要修改一堆字符串。
要记好安全红线:堆栈信息仅用于写日志,完全不会返回给 API 调用方。若将内部调用栈暴露出去,那就如同把系统架构图递给黑客,这属于严重的安全漏洞。API 给客户端返回的应当是“创建订单失败,请稍后重试”,而非堆栈中暴露出的数据库表名。
svr := server.New("localhost", 8080, time.Minute, 120)
日志:结构化大于字符串拼接
日志的级别必须要清晰地分辨出来,错误仅仅在最顶层进行一次.log记录。一层一层地对同一个错误进行.log记录这属于反模式,到最后日志里面会出现一大堆重复出错的报告,完全没办法知晓问题的源头究竟在什么地方。在底层返回错误之际使用WithStack进行包装,在最顶层统一使用Error来记录,既干净又便于进行debug。
svr := server.New(
server.WithHost("localhost"),
server.WithPort(8080),
server.WithTimeout(time.Minute),
server.WithMaxConn(120),
)
建议选用 zap 或者 logrus ,于生产环境开启 JSON 格式。字符串拼接而成的日志在ELK 、Loki 、阿里云 SLS 等平台难以如愿进行有效搜寻,凭借 JSON 格式能够轻易依照字段实施过滤。请求级日志需要携 trace id ,借助中间件注入,达成全链路追踪。
HTTP 调用:优先用 Context 超时
在执行HTTP调用之际,切莫依赖http.Client的全局超时设定,运用Context实施更为精细的控制。构建出一个带有超时的context,将其传递给http.NewRequestWithContext,每一个请求皆能够拥有独立的超时管控,彼此之间互不产生干扰。
这般能够更为妥善地偕同上层超时以及取消传播。举例而言,要是用户请求的总体超时时间为3秒,那么在内部调用下游服务之际,便能够运用2秒的上下文环境,剩余1秒用于业务处理。一旦用户将请求取消,所有针对下游的调用均会自动取消,以此规避资源的无端浪费。
单元测试:表驱动测试才是王道
if err != nil {
log.Errorf("ERROR :: Google Docs :: NewGoogleApiHandler :: Unable to create service :: %v", err)
return nil, err
}
在Go社区里,表驱动测试属于标准模式,它会定义测试用例切片,此切片中的每个用例囊括名称、输入以及期望输出,之后会循环执行,若要新增测试用例,仅仅只需添加一行数据,而测试代码无需做任何改动,并且维护成本极其低。
可推出工具go-cmp来开展深度比较,其错误信息清晰且直观。核心原则为测试务必稳定并不依赖外部环境与执行顺序。借助表驱动以及mock生成,单测编写起来速度如同闪电般迅速,覆盖率能轻松达到80%。
if err != nil {
return nil, fmt.Errorf("failed to create google docs service: %w", err)
// 或用 errors.Wrap / yerrors.Errorf
}
用以产出生产级别的Go代码,其关键并非在于“能够运行”,而是着重于“易于维护”、“便于调试”以及“利于协作”。要是采用这些实践方式,那么你便能够削减凌晨三点被唤起来查看日志的频次 ,能够使得新成员在三天之内提交代码却不会对主分支造成破坏 ,能够在架构评审之际更具底气地宣称“这契合工业界标准”。
这些并非教条,而是众多团队于规模化进程里历经挫折后归纳出的“止血关键点”。能够从你们当下痛感最为强烈之处着手逐步予以落实,比如说首先强行推行统一格式化,接着引入结构化日志,渐渐达成团队共识。
欢迎于评论区域之中,畅聊关于你所在团队当下,于Go项目范畴之内,最为令人纠结头疼之问题究竟是哪一个?
log.Errorf("Failed to write summary. Error: %s", err)



还没有评论,来说两句吧...