本文由 简悦 SimpRead 转码, 原文地址 cloud.tencent.com
各个公司或组织,都有各自不同的 Go 编码规范,但大同小异。规范是一种倡导,不遵守并不代表错误,但当大家都遵守规范时,你会发现,整个世界将变得整洁有序。
文章目录 前言 各个公司或组织,都有各自不同的 Go 编码规范,但大同小异。规范是一种倡导,不遵守并不代表错误,但当大家都遵守规范时,你会发现,整个世界将变得整洁有序。
本文结合官方编码建议,大厂编码规范和自身项目经验,尽可能以简短的语言给出一套行之有效 Go 编码规范建议,让您的代码高效易读。
本文所述内容均为参考意见,并非标准。其中许多是 Go 的通用准则,而其他扩展准则依赖于下面官方指南:
布局篇 Go 项目布局建议
风格篇 Go 编码规范建议——风格篇
功能篇 使用 time 处理时间 时间处理很复杂。关于时间的错误假设通常包括以下几点。
一天有 24 小时 一小时有 60 分钟 一周有七天 一年 365 天 还有更多 例如,在一个时间点上加上 24 小时并不总是产生一个新的日历日。
因此,在处理时间时始终使用 “time” 包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。
在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。
1 2 3 4 5 6 7 8 9 func isActive (now, start, stop int ) bool { return start <= now && now < stop }func isActive (now, start, stop time.Time) bool { return (start.Before(now) || start.Equal(now)) && now.Before(stop) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func poll (delay int ) { for { time.Sleep(time.Duration(delay) * time.Millisecond) } } poll(10 ) func poll (delay time.Duration) { for { time.Sleep(delay) } } poll(10 *time.Second)
对外部系统使用 time.Time 和 time.Duration 尽可能在与外部系统交互中使用 time.Duration
和 time.Time
,例如:
Command-line 标志: flag
通过 time.ParseDuration
支持 time.Duration
JSON: encoding/json
通过其 UnmarshalJSON method
方法支持将 time.Time
编码为 RFC 3339 字符串 SQL: database/sql
支持将 DATETIME
或 TIMESTAMP
列转换为 time.Time
,如果底层驱动程序支持则返回 YAML: gopkg.in/yaml.v2
支持将 time.Time
作为 RFC 3339 字符串,并通过 time.ParseDuration
支持 time.Duration。 当不能在这些交互中使用 time.Duration
时,请使用 int
或 float64
,并在字段名称中包含单位。
例如,由于 encoding/json
不支持 time.Duration
,因此该单位包含在字段的名称中。
1 2 3 4 5 6 7 8 9 10 11 type Config struct { Interval int `json:"interval"` }type Config struct { IntervalMillis int `json:"intervalMillis"` }
避免在公共结构中嵌入类型 嵌入类型会泄漏实现细节、禁止类型演化、产生模糊的文档,应该尽可能地避免。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type ConcreteList struct { *AbstractList }type ConcreteList struct { list *AbstractList }func (l *ConcreteList) Add(e Entity) { l.list.Add(e) }func (l *ConcreteList) Remove(e Entity) { l.list.Remove(e) }
无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化。
向嵌入式接口添加方法是一个破坏性的改变。 删除嵌入类型是一个破坏性的改变。 即使使用满足相同接口的替代方法替换嵌入类型,也是一个破坏性的改变。 尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。
初始化 初始化 struct 1 2 3 4 5 6 7 8 9 k := User{"John" , "Doe" , true } k := User{ FirstName: "John" , LastName: "Doe" , Admin: true , }
例外:如果有 3 个 或更少的字段,则可以在测试表中省略字段名称。
1 2 3 4 5 6 7 tests := []struct { op Operation want string }{ {Add, "add" }, {Subtract, "subtract" }, }
1 2 3 4 5 6 7 8 9 10 11 12 13 user := User{ FirstName: "John" , LastName: "Doe" , MiddleName: "" , Admin: false , } user := User{ FirstName: "John" , LastName: "Doe" , }
例外:在字段名提供有意义上下文的地方包含零值。例如,表驱动测试中的测试用例可以受益于字段的名称,即使它们是零值的。
1 2 3 4 5 6 7 tests := []struct { give string want int }{ {give: "0" , want: 0 }, }
1 2 3 4 5 // Bad var user := User {} // Good var user User
在初始化结构引用时,请使用&T{}
代替new(T)
,以使其与结构体初始化一致。
1 2 3 4 5 6 7 8 9 10 11 sval := T{Name: "foo" } sptr := new (T) sptr.Name = "bar" sval := T{Name: "foo" } sptr := &T{Name: "bar" }
初始化 map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var ( m1 = map [T1]T2{} m2 map [T1]T2 )var ( m1 = make (map [T1]T2) m2 map [T1]T2 )
1 2 3 4 5 6 7 8 9 10 11 12 m := make(map [T1]T2, 3 ) m[k1] = v1 m[k2] = v2 m[k3] = v3 m := map [T1]T2{ k1: v1 , k2: v2 , k3: v3 , }
初始化 slice 非零值 slice 使用make()
初始化,并指定容量 1 2 3 4 5 nums := []int {} nums := make ([]int , 0 , SIZE)
零值切片(用 var 声明的切片)可立即使用,无需调用make()
创建 1 2 3 4 5 6 7 nums := []int {}var nums []int
变量申明 本地变量声明应使用短变量声明形式(:=
)
尽量缩小变量作用范围。
1 2 3 4 5 var s = "foo" s := "foo"
避免使用 init() 尽可能避免使用 init()。当 init() 是不可避免或可取的,代码应先尝试:
无论程序环境或调用如何,都要完全确定。 避免依赖于其他 init() 函数的顺序或副作用。虽然 init() 顺序是明确的,但代码可以更改, 因此 init() 函数之间的关系可能会使代码变得脆弱和容易出错。 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数 / 输入等。 避免 I/O,包括文件系统、网络和系统调用。 错误处理 error 处理 error 作为函数的值返回,必须对 error 进行处理,或将返回值赋值给明确忽略。对于defer xx.Close()
可以不用显式处理 error 作为函数的值返回且有多个返回值的时候,error 必须是最后一个参数 1 2 3 4 5 6 7 8 9 10 err := ioutil.WriteFile(name, data, 0644)if err != nil { return err }if err := ioutil.WriteFile(name, data, 0644); err != nil { return err }
1 2 3 4 5 6 7 func do () (error , int ) { }func do () (int , error ) { }
如果函数返回值需用于初始化其他变量,则采用下面的方式: 1 2 3 4 5 6 7 8 9 10 11 12 13 if err != nil { } else { }if err != nil { return }
错误返回的判断独立处理,不与其他变量组合逻辑判断。 1 2 3 4 5 6 x, err := f ()if err != nil { return }
带参数的 error 生成方式为:fmt.Errorf("module xxx: %v", err)
,而不是 errors.New(fmt.Sprintf("module xxx: %v",err))
。 panic 处理 在业务逻辑处理中禁止使用 panic 在 main 包中只有当完全不可运行的情况可使用 panic,例如:文件无法打开,数据库 无法连接导致程序无法正常运行 对于其它的包,可导出的接口一定不能有 panic 在包内传递错误时,不推荐使用 panic 来传递 error 1 2 3 4 5 6 7 8 9 10 11 12 13 14 x, y, err := f ()if err != nil || y == nil { return err } x, y, err := f ()if err != nil { return err }if y == nil { return fmt.Errorf("some error" ) }
建议在 main 包中使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题 panic 捕获只能到 goroutine 最顶层,每个自行启动的 goroutine,必须在入口处捕获 panic,并打印详细堆栈信息或进行其它处理 recover 处理 recover 用于捕获 runtime 的异常,禁止滥用 recover 必须在 defer 中使用,一般用来捕获程序运行期间发生异常抛出的 panic 或程序主动抛出的 panic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type TError string func (e TError) Error() string { return string (e) }func do (str string ) { panic (TError("错误信息" )) }func Do (str string ) (err error ) { defer func () { if e := recover (); e != nil { err = e.(TError) } }() do(str) return nil }
类型断言失败处理 type assertion 的单个返回值形式针对不正确类型将产生 panic。因此,请始终使用 “comma ok” 惯用法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "log" )func main () { defer func () { if err := recover (); err != nil { log.Println("exec panic error: " , err) } }() getOne() panic (44 ) }func getOne () { defer func () { if err := recover (); err != nil { log.Println("exec panic error: " , err) } }() var arr = []string {"a" , "b" , "c" } log.Println("hello," , arr[4 ]) }2021 /10 /04 11 :07 :13 exec panic error : runtime error : index out of range [4 ] with length 3 2021 /10 /04 11 :07 :13 exec panic error : 44
性能篇 优先使用 strconv 而不是 fmt 将原语转换为字符串或从字符串转换时,strconv
比fmt
快。
1 2 3 4 5 6 7 8 t := i.(string ) t, ok := i.(string )if !ok { }
指定 slice 容量 在尽可能的情况下,在使用make()
初始化切片时提供容量信息,特别是在追加切片时。
1 2 3 4 5 6 7 8 9 10 11 // Bad // BenchmarkFmtSprint-4 143 ns/op 2 allocs/op for i := 0 ; i < b .N ; i ++ { s := fmt.Sprint (rand.Int ()) } // Good // BenchmarkStrconv-4 64.2 ns/op 1 allocs/op for i := 0 ; i < b .N ; i ++ { s := strconv.Itoa (rand.Int ()) }
指定 map 容量 向make()
提供容量提示会在初始化时尝试调整map
的大小,这将减少在将元素添加到map
时为map
重新分配内存。
注意,与 slice 不同。map capacity 提示并不保证完全的抢占式分配,而是用于估计所需的 hashmap bucket 的数量。 因此,在将元素添加到 map 时,甚至在指定 map 容量时,仍可能发生分配。
注释 在编码阶段同步写好类型、变量、函数、包注释,注释可以通过godoc
导出生成文档。
程序中每一个被导出的 (大写的) 名字,都应该有一个文档注释。
所有注释掉的代码在提交 code review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。
包注释 每个包都应该有一个包注释。 包如果有多个 go 文件,只需要出现在一个 go 文件中(一般是和包同名的文件)即可,格式为:“// Package 包名 包信息描述”。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for n := 0 ; n < b.N; n++ { data := make ([]int , 0 ) for k := 0 ; k < size; k++{ data = append (data, k) } }for n := 0 ; n < b.N; n++ { data := make ([]int , 0 , size) for k := 0 ; k < size; k++{ data = append (data, k) } }
函数注释 每个需要导出的函数或者方法(结构体或者接口下的函数称为方法)都必须有注释。注意,如果方法的接收器为不可导出类型,可以不注释,但需要质疑该方法可导出的必要性。 注释描述函数或方法功能、调用方等信息。 格式为:”// 函数名 函数信息描述”。 函数调用中的意义不明确的参数可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */
)
1 2 3 4 5 6 7 8 9 10 11 package mathpackage template
结构体注释 每个需要导出的自定义结构体或者接口都必须有注释说明。 注释对结构进行简要介绍,放在结构体定义的前一行。 格式为:”// 结构体名 结构体信息描述”。 结构体内的可导出成员变量名,如果是个生僻词或意义不明确的词,必须要单独给出注释,放在成员变量的前一行或同一行的末尾。 1 2 3 4 func NewAttrModel (ctx *common.Context) *AttrModel { }
变量和常量注释 每个需要导出的常量和变量都必须有注释说明。 注释对常量或变量进行简要介绍,放在常量或变量定义的前一行。 大块常量或变量定义时,可在前面注释一个总的说明,然后每一行常量的末尾详细注释该常量。 独行注释格式为:”// 变量名 描述”,斜线后面紧跟一个空格。 1 2 3 4 5 6 7 printInfo("foo" , true , true ) printInfo("foo" , true , true )
类型注释 每个需要导出的类型定义(type definition)和类型别名(type aliases)都必须有注释说明。 该注释对类型进行简要介绍,放在定义的前一行。 格式为:”// 类型名 描述”。 1 2 3 4 5 6 // User 用户结构定义了用户基础信息 type User struct { Name string Email string Demographic string // 族群 }
命名规范 命名是代码规范中很重要的一部分,统一的命名规范有利于提高代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。
包命名 保持 package 的名字和目录一致。 尽量采取有意义、简短的包名,尽量不要和标准库冲突。 包名应该为小写单词,不要使用下划线或者混合大小写,使用多级目录来划分层级。 简单明了的包命名,如:time、list、http。 不要使用无意义的包名,如util、common、misc、global
。package 名字应该追求清晰且越来越收敛,符合‘单一职责’原则。而不是像common
一样,什么都能往里面放,越来越膨胀,让依赖关系变得复杂,不利于阅读、复用、重构。注意,xx/util/encryption
这样的包名是允许的。 文件命名 采用有意义、简短的文件名。 文件名应该采用小写,并且使用下划线分割各个单词。 函数命名 函数名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。 代码生成工具自动生成的代码可排除此规则(如协议生成文件 xxx.pb.go , gotests 自动生成文件 xxx_test.go 里面的下划线)。 结构体命名 采用驼峰命名方式,首字母根据访问控制采用大写或者小写。 结构体名应该是名词或名词短语,如 Customer、WikiPage、Account、AddressParser,它不应是动词。 避免使用 Data、Info 这类意义太宽泛的结构体名。 结构体的定义和初始化格式采用多行,例如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const FlagConfigFile = "--config" const ( FlagConfigFile1 = "--config" FlagConfigFile2 = "--config" FlagConfigFile3 = "--config" FlagConfigFile4 = "--config" )var FullName = func (username string ) string { return fmt.Sprintf("fake-%s" , username) }
接口命名 命名规则基本保持和结构体命名规则一致。 单个函数的接口名以 er 作为后缀,例如 Reader,Writer。 1 2 3 4 5 type StorageClass string type FakeTime = time .Time
两个函数的接口名综合两个函数名。 三个以上函数的接口名,类似于结构体名。 1 2 3 4 5 6 7 8 9 10 11 // User 多行声明 type User struct { Name string Email string } // 多行初始化 u := User{ UserName: "john" , Email: "john@example.com" , }
变量命名 变量名必须遵循驼峰式,首字母根据访问控制决定大写或小写。 特有名词时,需要遵循以下规则: (1)如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient; (2)其他情况都应该使用该名词原有的写法,如 APIClient、repoID、UserID; (3)错误示例:UrlArray,应该写成 urlArray 或者 URLArray; (4)详细的专有名词列表可参考这里 。 若变量类型为 bool 类型,则名称应以 Has,Is,Can 或者 Allow 开头。 私有全局变量和局部变量规范一致,均以小写字母开头。 代码生成工具自动生成的代码可排除此规则(如 xxx.pb.go 里面的 Id)。 变量名更倾向于选择短命名。特别是对于局部变量。 c 比 lineCount 要好,i 比 sliceIndex 要好。基本原则是:变量的使用和声明的位置越远,变量名就需要具备越强的描述性。 常量命名 1 2 3 4 5 type Reader interface { Read(p []byte ) (n int , err error ) }
1 2 3 4 5 6 7 8 9 type Car interface { Start ([]byte) Stop () error Recover () }
私有全局常量和局部变量规范一致,均以小写字母开头。 1 2 const AppVersion = "1.0.0"
方法接收器命名 推荐以类名第一个英文首字母的小写作为接收器的命名。 接收器的命名在函数超过 20 行的时候不要用单字符。 命名不能采用 me,this,self 这类易混淆名称。 避免使用内置名称 Go 语言规范 language specification 概述了几个内置的,不应在 Go 项目中使用的名称标识 predeclared identifiers 。
1 2 3 4 5 6 7 8 type Scheme string const ( HTTP Scheme = "http" HTTPS Scheme = "https" )
编译器在使用预先分隔的标识符时不会生成错误, 但是诸如go vet
之类的工具会正确地指出这些和其他情况下的隐式问题。
1 const appVersion = "1.0.0"
流程控制 if 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Types: bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr Constants: true false iota Zero value: nil Functions: append cap close complex copy delete imag len make new panic print println real recover
if 对两个值进行判断时,约定如下顺序:变量在左,常量在右 1 2 3 4 5 6 7 8 9 10 11 12 var error string func handleErrorMessage (error string ) { }var errorMessage string func handleErrorMessage (msg string ) { }
if 对于 bool 类型的变量,应直接进行真假判断 1 2 3 if err := file .Chmod(0664); err != nil { return err }
如果在 if 的两个分支中都设置变量,则可以将其替换为单个 if。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if nil != err { }if 0 == errorCode { }if err != nil { } if errorCode == 0 { }
for 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var allowUserLogin bool// 不要采用这种方式if allowUserLogin == true { // do something }// 不要采用这种方式if allowUserLogin == false { // do something }// 而要采用下面的方式if allowUserLogin { // do something }// 而要采用下面的方式if !allowUserLogin { // do something }
range 如果只需要第一项(key),就丢弃第二个(value) 1 2 3 4 5 6 7 8 9 10 11 12 13 var a intif b { a = 100 } else { a = 10 }a := 10 if b { a = 100 }
1 2 3 4 sum := 0 for i := 0 ; i < 10 ; i++ { sum += 1 }
switch 1 2 3 4 5 for key := range m { if key .expired() { delete (m, key ) } }
return 1 2 3 4 sum : = 0 for _, v : = range array { sum += v }
goto 业务代码禁止使用 goto,其他框架或底层源码推荐尽量不用。
主函数退出方式 Go 程序使用os.Exit
或者log.Fatal*
立即退出,而不是panic
。 一次性退出,如果可能的话,你的main()
函数中最多一次 调用os.Exit
或者log.Fatal
。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 这会精简main()
函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。 1 2 3 4 5 6 7 8 9 10 switch os := runtime .GOOS; os { case "darwin" : fmt.Println ("MAC OS" ) case "linux" : fmt.Println ("Linux." ) default : fmt.Printf("%s.\n" , os) }
函数 入参 & 返回值 入参和返回值以小写字母开头。 入参和返回值均不能超过 5 个。 尽量用值传递,非指针传递。 类型为 map,slice,chan,interface 不要传递指针。 返回两个或三个值,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。 1 2 3 4 5 6 7 8 9 10 11 12 13 f, err := os.Open (name)if err != nil { return err } defer f.Close ()d , err := f.Stat()if err != nil { return err } codeUsing(f, d )
defer 当存在资源管理时,应紧跟 defer 函数进行资源的释放。 判断是否有错误发生之后,再 defer 释放资源。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainfunc main () { args := os.Args[1 :] if len (args) != 1 { log.Fatal("missing file" ) } name := args[0 ] f, err := os.Open(name) if err != nil { log.Fatal(err) } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { log.Fatal(err) } }package mainfunc main () { if err := run(); err != nil { log.Fatal(err) } }func run () error { args := os.Args[1 :] if len (args) != 1 { return errors.New("missing file" ) } name := args[0 ] f, err := os.Open(name) if err != nil { return err } defer f.Close() b, err := ioutil.ReadAll(f) if err != nil { return err } }
1 2 3 4 5 6 7 8 // Parent1 ... func (n *Node ) Parent1 () *Node // Parent2 ... func (n *Node ) Parent2 () (*Node , error ) // Location ... func (f *Foo) Location() (lat, long float64, err error)
代码行数 函数长度不能超过 80 行。 文件长度不能超过 800 行。 减少嵌套(圈复杂度) 1 2 3 4 5 6 resp, err := http.Get (url)if err != nil { return err } defer resp.Body.Close ()
魔法数字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func filterSomething (values []string ) { for _, v := range values { fields, err := db.Query(v) if err != nil { } defer fields.Close() } }func filterSomething (values []string ) { for _, v := range values { func () { fields, err := db.Query(v) if err != nil { ... } defer fields.Close() }() } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 func (s *BookingService) AddArea(areas ...string ) error { s.Lock() defer s.Unlock() for _, area := range areas { for _, has := range s.areas { if area == has { return srverr.ErrAreaConflict } } s.areas = append (s.areas, area) s.areaOrders[area] = new (order.AreaOrder) } return nil }func (s *BookingService) AddArea(areas ...string ) error { s.Lock() defer s.Unlock() for _, area := range areas { if s.HasArea(area) { return srverr.ErrAreaConflict } s.areas = append (s.areas, area) s.areaOrders[area] = new (order.AreaOrder) } return nil }func (s *BookingService) HasArea(area string ) bool { for _, has := range s.areas { if area == has { return true } } return false }
函数分组与顺序 函数应按粗略的调用顺序排序。 同一文件中的函数应按接收者分组。 因此,导出的函数应先出现在文件中,放在struct
, const
, var
定义的后面。
在定义类型之后,但在接收者的其余方法之前,可能会出现一个newXYZ()/NewXYZ()
。
由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。
1 2 3 4 5 6 func getArea (r float64 ) float64 { return 3.14 * r * r }func getLength (r float64 ) float64 { return 3.14 * 2 * r }
单元测试 单元测试文件名命名规范为 example_test.go。 测试用例的函数名称必须以 Test 开头,例如 TestExample。 单测文件行数限制是普通文件的 2 倍(1600 行)。单测函数行数限制也是普通函数的 2 倍(160 行)。圈复杂度、列数限制、 import 分组等其他规范细节和普通文件保持一致。 由于单测文件内的函数都是不对外的,所有可导出函数可以没有注释,但是结构体定义时尽量不要导出。 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。 表驱动测试 使用 table 驱动的方式编写 case 代码看上去会更简洁。
1 2 3 4 5 6 7 8 9 10 const PI = 3.14 func getArea (r float64 ) float64 { return PI * r * r }func getLength (r float64 ) float64 { return PI * 2 * r }
依赖管理 go1.11 以上必须使用 go modules 模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // Bad func (s *something) Cost () { return calcCost(s.weights) }type something struct{ ... }func calcCost(n []int ) int {...} func (s *something) Stop () {...} func newSomething() *something { return &something{} } // Good type something struct{ ... }func newSomething() *something { return &something{} }func (s *something) Cost () { return calcCost(s.weights) }func (s *something) Stop () {...} func calcCost(n []int ) int {...}
代码提交 建议使用 go modules 作为依赖管理的项目不提交 vendor 目录 使用 go modules 管理依赖的项目, go.sum
文件必须提交,不要添加到.gitignore
规则中 应用服务 应用服务建议有 README.md 说明文档,介绍服务功能、使用方法、部署时的限制与要求、基础环境依赖等 应用服务必须要有接口测试 常用工具 Go 本身在代码规范方面做了很多努力,很多限制都是语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错。此外 Go 还是提供了很多好用的工具帮助我们进行代码的规范。
gofmt ,大部分的格式问题可以通过 gofmt 解决, gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,于是所有格式有关问题,都以 gofmt 的结果为准。 goimports ,此工具在 gofmt 的基础上增加了自动删除和引入包。 go vet ,vet 工具可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前 return 的逻辑, struct 的 tag 是否符合标准等。编译前先执行代码静态分析。 golint ,类似 javascript 中的 jslint 的工具,主要功能就是检测代码中不规范的地方。 参考文献 github.com/uber-go/guide
github.com/golang-standards/project-layout
书栈网. Go 语言 (Golang) 编码规范
本文参与 腾讯云自媒体同步曝光计划 ,分享自作者个人站点 / 博客。
原始发表:2021/10/05 ,如有侵权请联系 cloudcommunity@tencent.com 删除