Go语言“正统”在中国?这6点教你写好Go代码!
👉导读
数据显示,中国 Gopher 人数全球占比最高,Go 语言在国内的火热态势甚至让创始人 Rob Pike 惊讶到不敢想象,颇有一种 Golang 正统在中国的感觉。Go 语言也是腾讯内部最热门的编程语言,随着云计算技术的快速普及,使用 Go 语言编写的 IT 基础设施也变得更为广泛流行,让 Go 语言的热度和人才需求度都进一步得到提升。本文作者从设计、规范、陷阱到相关实现以例证说明并结合自己思考,详细解释了该如何写好 Go 代码,值得你的点赞分享转发收藏!👉目录
1 Golang 实现 SOLID 设计原则2 Golang 实现常见设计模式3 Golang 易疏忽规范4 Golang 编码陷阱5 Golang 编码相关工具6 如何做好 CR?7 结语01
1.1 单一职责原则
type Trade struct {
TradeID int
Symbol string
Quantity float64
Price float64
}
type TradeRepository struct {
db *sql.DB
}
func (tr *TradeRepository) Save(trade *Trade) error {
_, err := tr.db.Exec("INSERT INTO trades (trade_id, symbol, quantity, price) VALUES (?, ?, ?, ?)", trade.TradeID, trade.Symbol, trade.Quantity, trade.Price)
if err != nil {
return err
}
return nil
}
type TradeValidator struct {}
func (tv *TradeValidator) Validate(trade *Trade) error {
if trade.Quantity <= 0 {
return errors.New("Trade quantity must be greater than zero")
}
if trade.Price <= 0 {
return errors.New("Trade price must be greater than zero")
}
return nil
}
1.2 开闭原则
type TradeProcessor interface {
Process(trade *Trade) error
}
type FutureTradeProcessor struct {}
func (ftp *FutureTradeProcessor) Process(trade *Trade) error {
// process future trade
return nil
}
type OptionTradeProcessor struct {}
func (otp *OptionTradeProcessor) Process(trade *Trade) error {
// process option trade
return nil
}
1.3 里氏替换原则
type Trade interface {
Process() error
}
type FutureTrade struct {
Trade
}
func (ft *FutureTrade) Process() error {
// process future trade
return nil
}
1.4 接口隔离原则
type Trade interface {
Process() error
}
type OptionTrade interface {
CalculateImpliedVolatility() error
}
type FutureTrade struct {
Trade
}
func (ft *FutureTrade) Process() error {
// process future trade
return nil
}
type OptionTrade struct {
Trade
}
func (ot *OptionTrade) Process() error {
// process option trade
return nil
}
func (ot *OptionTrade) CalculateImpliedVolatility() error {
// calculate implied volatility
return nil
}
1.5 依赖倒置原则
type TradeService interface {
Save(trade *Trade) error
}
type TradeProcessor struct {
tradeService TradeService
}
func (tp *TradeProcessor) Process(trade *Trade) error {
err := tp.tradeService.Save(trade)
if err != nil {
return err
}
// process trade
return nil
}
type SqlServerTradeRepository struct {
db *sql.DB
}
func (str *SqlServerTradeRepository) Save(trade *Trade) error {
_, err := str.db.Exec("INSERT INTO trades (trade_id, symbol, quantity, price) VALUES (?, ?, ?, ?)", trade.TradeID, trade.Symbol, trade.Quantity, trade.Price)
if err != nil {
return err
}
return nil
}
type MongoDbTradeRepository struct {
session *mgo.Session
}
func (mdtr *MongoDbTradeRepository) Save(trade *Trade) error {
collection := mdtr.session.DB("trades").C("trade")
err := collection.Insert(trade)
if err != nil {
return err
}
return nil
}
02
2.1 单例设计模式
var once sync.Once
var instance interface{}
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
2.2 工厂模式
package main
// Factory interface
type simpleInterest struct {
principal int
rateOfInterest int
time int
}
type compoundInterest struct {
principal int
rateOfInterest int
time int
}
// Interface
type InterestCalculator interface {
Calculate()
}
func (si *simpleInterest) Calculate() {
// logic to calculate simple interest
}
func (si *compoundInterest) Calculate() {
// logic to calculate compound interest
}
func NewCalculator(kind string) InterestCalculator {
if kind == "simple" {
return &simpleInterest{}
}
return &compoundInterest{}
}
func Factory_Interface() {
siCalculator := NewCalculator("simple")
siCalculator.Calculate() // Invokes simple interest calculation logic
ciCalculator := NewCalculator("compound")
ciCalculator.Calculate() // Invokes compound interest calculation logic
}
2.3 代理模式
其他对象提供一种代理以控制对这个对象的访问。类图:
// zkClient backend request struct.
type zkClient struct {
ServiceName string
Client client.Client
opts []client.Option
}
// NewClientProxy create new zookeeper backend request proxy,
// required parameter zookeeper name service: trpc.zookeeper.xxx.xxx.
func NewClientProxy(name string, opts ...client.Option) Client {
c := &zkClient{
ServiceName: name,
Client: client.DefaultClient,
opts: opts,
}
c.opts = append(c.opts, client.WithProtocol("zookeeper"), client.WithDisableServiceRouter())
return c
}
// Get execute zookeeper get command.
func (c *zkClient) Get(ctx context.Context, path string) ([]byte, *zk.Stat, error) {
req := &Request{
Path: path,
Op: OpGet{},
}
rsp := &Response{}
ctx, msg := codec.WithCloneMessage(ctx)
defer codec.PutBackMessage(msg)
msg.WithClientRPCName(fmt.Sprintf("/%s/Get", c.ServiceName))
msg.WithCalleeServiceName(c.ServiceName)
msg.WithSerializationType(-1) // non-serialization
msg.WithClientReqHead(req)
msg.WithClientRspHead(rsp)
if err := c.Client.Invoke(ctx, req, rsp, c.opts...); err != nil {
return nil, nil, err
}
return rsp.Data, rsp.Stat, nil
}
2.4 观察者模式
package main
import "fmt"
type Item struct {
observerList []Observer
name string
inStock bool
}
func newItem(name string) *Item {
return &Item{
name: name,
}
}
func (i *Item) updateAvailability() {
fmt.Printf("Item %s is now in stock\n", i.name)
i.inStock = true
i.notifyAll()
}
func (i *Item) register(o Observer) {
i.observerList = append(i.observerList, o)
}
func (i *Item) notifyAll() {
for _, observer := range i.observerList {
observer.update(i.name)
}
}
03
3.1 声明
错误使用 util 命名的包,不容易正常识别功能的用途,导致 util 包越来越臃肿。 slice 的创建使用 var arr []int,初始化切片使用 var s []string 而不是 s := make([]string),初始化,如果确定大小建议使用 make 初始化。 import . 只能用于测试文件,且必须是为了解决循环依赖,才能使用。
3.2 函数定义
不要通过参数返回数据。
尽量用 error 表示执行是否成功,而不是用 bool 或者 int。
多使用指针接收器,尽量避免使用值接收器。
除0、1、“”不要使用字面量。
if else 通常可以简写为 if return。
尽量将 if 和变量定义应该放在一行。
err := r.updateByAttaIDs(fMd5OneTime, sMd5OneTime)
if err != nil {
不要添加没必要的空行。
使用 == "" 判断字符串是否为空。
通过 %v 打印错误信息,%v 建议加:。
Fail Fast 原则,如果出现失败应该立即返回 error,如果继续处理,则属于特殊情况需要添加注释。
array 和 map 的变量命名时,添加后缀 s。 _, xxx for xxxs 一般要求 xxx 相同。 正则表达式变量名以 RE 结尾。 不要用注释删除代码。 TODO 格式:TODO(rtx_name): 什么时间/什么时机,如何解决。19.导出的函数/变量的职责必须与包&文件职责高度一致。
时间类型尽量使用内置定义,如,time.Second,不要使用 int。
建议所有不对外开源的工程的 module name 使用 xxxxxx/group/repo ,方便他人直接引用。
应用服务接口建议有 README.md。
3.6 安全问题
代码中是否存在 token 密码是否加密。
日志中是否输出用户敏感信息。
PB 是否开启 validation。
字符串占位符,如果输入数据来自外部,建议使用 %q 进行安全转义。
04
4.1 值拷贝
package main
import (
"fmt"
)
func main() {
x := [3]int{1, 2, 3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr)
}(x)
fmt.Println(x) // 1 2 3
}
4.2 管道操作
管道操作,谨记口诀:“读关闭空值,读写空阻塞,写关闭异常,关闭空、关闭已关闭异常”。个人建议管道除非在一些异步处理的场景建议使用外,其它场景不建议过多使用,有可能会影响代码的可读性。
检测管道关闭示例:
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
4.3 匿名函数变量捕获
type A struct {
id int
}
func main() {
channel := make(chan A, 5)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for a := range channel {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(a.id) // 输出的数字是无法确定的,输出依赖具体的调度时机。
// go vet 提示 loop variable a captured by func literal
}()
}
}()
for i := 0; i < 10; i++ {
channel <- A{id:i}
}
close(channel)
wg.Wait()
}
4.4 defer 执行流程
package main
import (
"fmt"
)
func main() {
fmt.Println("c return:", *(c())) // 打印结果为 c return: 2
}
func c() *int {
var i int
defer func() {
i++
fmt.Println("c defer2:", i) // 打印结果为 c defer: 2
}()
defer func() {
i++
fmt.Println("c defer1:", i) // 打印结果为 c defer: 1
}()
return &i
}
package main
import "fmt"
func main() {
fmt.Println(test())
}
func test() (result int) {
defer func() {
result++
}()
return 0 // result = 0
// result++
}
4.5 recover 正确执行方式
// 无效
func main() {
recover()
panic(1)
}
// 无效
func main() {
defer recover()
panic(1)
}
// 无效
func main() {
defer func() {
func() { recover() }()
}()
panic(1)
}
// 有效
func main() {
defer func() {
recover()
}()
panic(1)
}
4.6 sync.Mutex 错误传递
package main
import (
"fmt"
"sync"
"time"
)
type Container struct {
sync.Mutex // <-- Added a mutex
counters map[string]int
}
func (c Container) inc(name string) {
c.Lock() // <-- Added locking of the mutex
defer c.Unlock()
c.counters[name]++
}
func main() {
c := Container{counters: map[string]int{"a": 0, "b": 0}}
doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
}
go doIncrement("a", 100000)
go doIncrement("a", 100000)
// Wait a bit for the goroutines to finish
time.Sleep(300 * time.Millisecond)
fmt.Println(c.counters)
}
05
5.1 go vet
asmdecl report mismatches between assembly files and Go declarations
assign check for useless assignments
atomic check for common mistakes using the sync/atomic package
bools check for common mistakes involving boolean operators
buildtag check that +build tags are well-formed and correctly located
cgocall detect some violations of the cgo pointer passing rules
composites check for unkeyed composite literals
copylocks check for locks erroneously passed by value
httpresponse check for mistakes using HTTP responses
loopclosure check references to loop variables from within nested functions
lostcancel check cancel func returned by context.WithCancel is called
nilfunc check for useless comparisons between functions and nil
printf check consistency of Printf format strings and arguments
shift check for shifts that equal or exceed the width of the integer
slog check for incorrect arguments to log/slog functions
stdmethods check signature of methods of well-known interfaces
structtag check that struct field tags conform to reflect.StructTag.Get
tests check for common mistaken usages of tests and examples
unmarshal report passing non-pointer or non-interface values to unmarshal
unreachable check for unreachable code
unsafeptr check for invalid conversions of uintptr to unsafe.Pointer
unusedresult check for unused results of calls to some functions
5.2 goimports
5.3 gofmt
06
CR 的目的是让我们的代码更具有规范、排查出错误、代码设计的统一,从而降低不好代码所带来的误解、重复、错误等问题。无论是 contributor 或者是 codereviewer,都有职责去执行好 CR 的每个环节,这样我们才能写出更好更优秀的代码。
前置工作
发起人自己先做一次 review。
做好单测、自测,不要依赖 CodeReview 机制排查问题。
是否有现成的依赖包、工具、复用的代码使用。
仓库配置相应的 CodeCC、单测覆盖率检测流水线。
发起 Codereview
准备好本次 CR 的背景知识,如 TAPD、设计文档等。
COMMIT 里详细介绍本次改动的目的。
控制规模,一次提交最好能在30分钟内 review 完成。
CodeReviewer
友好语气。
认真提出合理的建议与改进方案,是对代码编写者的尊重。
避免纯主观判断。
不要高高在上。
不要吝啬称赞。
适度容忍、没有必要必须完美。
无重要的设计、bug 可以先 approve,后续有时间修改。
冲突解决
寻求第三人评估。
组内讨论。
07