本文由 简悦 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 删除