本文共 12646 字,大约阅读时间需要 42 分钟。
GO开发规范
开发环境
- Visual Code 与微软开发的专用 Go 插件:VS Code + Go
- Jet Brains 出的专用 IDE:Goland
GOPATH
- 项目目录应该位于 $GOPATH/src 下。
- 项目专用的 GOPATH 设置可以使用软链技术来实现:
$ echo $GOPATH/tmp/gopath$ pwdxxx/project$ ln -s `pwd` $GOPATH$ tree $GOPATH/tmp/gopath (${GOPATH})└── src └── project -> xxx/project └── main.go$ go build project
编码规范
Golang 官方认为统一的编码风格是为了减少开发人员间的时间浪费,但编码风格之间没 有统一定论,存在不少争议,因此 Golang 提倡的是使用特定工具来解决这类问题。
因此 Golang 提供了默认的格式化工具 ,命令:
go fmt [-n] [-x] [packages]
gofmt
具体的标准详见
格式化
- 缩进:使用制表符(tab)
- 行长度:无限制
- 括号:控制结构语法上不需要括号
命名
- 所有命名应该简单清晰:
- 使用 而不是 once.DoOrWaitUntilDone 。
包
- 不应该使用下划线和驼峰命名
- 包名和源码目录名应该一致 +因包内导出名称会在包名的命名空间下,导出名称不要以包名作为标识:
名称
- 可导出名称需要大写开头的驼峰命名方式
- 局部名称使用小写开头的驼峰命名方式
- 缩写使用全大写或全小写(变量开头)的命名方式
函数
函数设计应清晰简洁:
- error 作为最后一个返回值
- 代表成功/失败的状态值要作为最后一个返回值
另外为了避免 goroutine 和数据的泄漏,同时为了测试的便利,尽量设计同步方法, 即:
- 内部 goroutine 生存期和函数生存期相同
- 尽量直接返回结果,而不要以其他同步方式
方法
- 接收者不要用 或者 这种笼统没有意义的名称
- 一般使用有代表性的单字母
构造函数
以 New + 类型名称的方式来命名
特别的,如果一个包中只有一个构造函数,可以直接使用New
接口
- 单函数的接口以函数名 + er 的方式命名
Read
与Reader
Write
与Writer
注释
语法
Golang 注释语法上继承了 C/C++: 、
- 块注释: /* … */
- 行注释: //
Golang 会将特定位置的注释看做文档的一部分,类似于 Python 的 docstring,使用
godoc 可以查询和导出对应的注释文档:godoc package [name ...]
包注释(package comment )
包注释是在 package 声明语句之前的注释。
/*Package regexp implements a simple library for regularexpressions.The syntax of the regular expressions accepted is: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ]term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')'*/package regexp
文档注释(doc comment)
在包中所有的顶级声明前的注释将会作为该声明的文档注释,尤其对于可导出的声明,注
释应以该名称开头。// Compile parses a regular expression and returns, ifsuccessful, a Regexp// object that can be used to match against text.func Compile(str string) (regexp *Regexp, err error) {
组合注释
对于有共性的声明,可以使用组合注释,常用在错误声明中。
// Error codes returned by failures to parse an expression.var ( ErrInternal = errors.New("regexp: internal error") ErrUnmatchedLpar = errors.New("regexp: unmatched '('") ErrUnmatchedRpar = errors.New("regexp: unmatched ')'") ...)
控制结构
分号
Golang 的正式语法使用分号来结束语句,但是通常不需要手动输入,词法分析器会自动 处理。
但需要注意的是,所有控制结构的左大括号必须和关键字( 、 、 与 )位于同一行,另起一行会导致在左大括号前追加一个分号。
同样的,大括号不能省略。if
- 条件语句不需要加上圆括号
- 省略不必要的 else 语句
- 可以加上合适的初始化语句
result := query()if err := check(result); err != nil { return err }// 不需要 else doSomeThing(result)
for
Golang 只有 for 一种循环结构。
步进
for i := 0; i < 10; i++ { ...}
Golang 没有逗号表达式,并且++和—操作是语句而非表达式。
##遍历
range
可以遍历数组,切片,字典,管道和字符串。 for key, value := range oldMap { ...}
遍历字符串
支持 UTF-8 的字符串:
for pos, str := range "SONY大法好" { fmt.Printf("%q: %d\n", str, pos)}
‘S’: 0 ‘O’: 1 ‘N’: 2 ‘Y’: 3 ‘大’: 6 ‘法’: 9 ‘好’: 12
switch
相比于 C,Golang 将 switch 改造得更为通用。
- 表达式不限制为常量或整数
- case 可以使用逗号来列举多个条件
- 无需显式 break,但使用 break 可以提前结束
func Factory(name string, value interface{ }) interface{ } { var object interface{ } switch name { case "A", "AA": object = NewA() case "B": object = newB() if value == nil { break } object.SetValue(value) } return object}
类型选择
对于接口变量,可以使用 switch 来判断其实际类型,这是一个很有用的技巧:
func ErrorWrap(e interface{ }) *TraceableError { var message string switch e := e.(type) { case TraceableError: return &e case *TraceableError: return e case error: message = e.Error() default: message = fmt.Sprintf("%v", e) } return ErrorNew(message, 2)}
退出循环
因为 break 关键字在 switch 块中有特殊含义,因此无法直接用 break 退出循环, 需要借助标签:
package mainimport ( "fmt")func main() { Loop: for index := 1; index < 10; index++ { switch index % 5 { case 1: break case 0: break Loop default: fmt.Printf("%v\n", index) } }}
2 3 4
select
select 用法类似于 switch
,专用于轮询多个管道的读取。
包
包需要在头部使用 package 关键字声明包名,一般包名和文件夹名称一致即可。
文件
包内所有文件都在同一个命名空间下,因此建议按照功能组织包内的文件代码,同时在多
人共同开发时分别编辑不同的文件,以减少冲突的可能。单元测试
功能文件和单元测试文件放在同一个文件夹内,单元测试文件名带上 _test 后缀,其中
包名要声明成 + _test 的形式。 单元测试可以用 . 导入需要测试的包,除此之外都不要使用 . 导入。Golang 推荐使用表驱动的测试方式:
var flagtests = []struct { in stringout string }{ { "%a", "[%a]"}, { "%-a", "[%-a]"}, { "%+a", "[%+a]"}, { "%#a", "[%#a]"}, { "% a", "[% a]"}, { "%0a", "[%0a]"}, { "%1.2a", "[%1.2a]"}, { "%-1.2a", "[%-1.2a]"}, { "%+1.2a", "[%+1.2a]"}, { "%-+1.2a", "[%+-1.2a]"}, { "%-+1.2abc", "[%+-1.2a]bc"}, { "%-1.2abc", "[%-1.2a]bc"},}func TestFlagParser(t *testing.T) { var flagprinter flagPrinter for _, tt := range flagtests { s := Sprintf(tt.in, &flagprinter) if s != tt.out { t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out) } }}
初始化
包内的每个文件都可以声明 init 函数来进行初始化工作,此函数在 import 阶段就会
执行,相当于在 main 函数开始之前执行。导入
除特殊原因外,不要导入没有使用的模块,会导致编译出错,一般使用 goimports 等 可自动处理。 但因为诸如 init 函数等有副作用的情况存在,只引入而不使用一个包的 情况是存在且合理的,我们要用空白标识符来命名这个引入即可,如 gorm 中指定数据库 驱动:
package mainimport ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite")
错误处理
应该区分错误和异常:
- 错误:作为流程的一部分,被调用方显式返回,调用方主动处理。
- 异常:预料之外出现或者在流程中不应该出现的错误
但是在抽象和封装的时候注意收敛错误类型,避免调用方需要频繁和过多地处理错误。
比如编译一个正则表达式是有可能出错的,但是如果这个正则表达式是由系统内部直接构 造的话,就不应该会出错,因此 regexp 提供了 MustCompile 来收敛编译出错的情 况。func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` +error.Error())} return regexp}
错误
错误是正常流程的一部分,但区别于正确结果的状态,直接 return , 如用户参数校验
失败,数据库查找失败。- 在 package 中新建一个 errors.go,导出依赖或函数的错误;
- 函数中不再直接 return errors.New(“xxx”) ,因为错误是 Immutable 的,直接 使用全局对象;
- 使用 switch…case…default 语句进行类似 Python 的 try…except 方式来处理 错误;
错误是一个接口,不是某种特定类型:
type error interface { Error() string}
因此很多系统内会自定义一个带有堆栈信息(通过 runtime)的全局错误类型。 此外,错误内容要使用小写。
异常
异常的出现说明出现了非预期的情况,会导致服务器出现500错误,如数据库连接断掉, 第三方接口调用异常。
Golang 使用 panic 和 recover 来处理异常。
- panic 接收一个错误,中断当前流程
- recover 一般和 defer 配合使用,恢复中断的流程并且处理对应的错误
package mainimport ( "fmt""regexp")func main() { regex := `(\d` // should be `(\d)` defer func() { e := recover() if e != nil { fmt.Printf("compile error: %v\n", regex) } }() regexp.MustCompile(regex)}
结构和接口
结构
初始化
p1 := new (MyStruct) // type *SyncedBufferp2 := &MyStruct{} // type *SyncedBuffervar s1 MyStruct // type SyncedBuffers2 := MyStruct{} // type SyncedBuffer
初始化时可以指定结构成员的初始值:
type MyStruct1 struct { Value int}type MyStruct2 struct { MyStruct1ID int }// 直接初始化s1 := MyStruct1{ Value: 0, }// 嵌套结构s2 := MyStruct2{ ID: 0, MyStruct1: MyStruct1 { Value: 1, }}
方法
获取器(getter)和设置器(setter)
Golang 没有对获取器和设置器提供支持,需要手动实现。
假设有名为 owner 的未导出字段,则其获取器应为大写的方法 Owner ,设置器为 SetOwner ,如:owner := obj.Owner()if owner != user { obj.SetOwner(user)}
接收者
方法的接收者既可以声明成值类型,也可以声明成指针类型。调用时,Golang 可以自动 进行转换。 但是需要注意的是,声明成值类型时,调用方法时传入的是调用者的拷贝, 而不是调用者本身,因此对接收者的修改将不生效。
接收者类型建议如下:- map , func , chan :不要使用指针
- 切片类型:如果不存在对切片的重分配,
- 则不要使用指针
- 如果方法会改变接收者,必须使用指针
- 如果接收者结构有类似 sync.Mutex 等用于同步的成员,必须使用
- 一般情况,从实用性的角度出发,建议接收者都声明成指针类型
defer
- 打开文件/连接等后需要 defer 来延时执行关闭
- 慎用 defer 来处理锁
- defer 求值是实时的,因此可以在循环中使用
package mainimport "fmt"func main() { word := "world" defer fmt.Printf("%v\n", word) word = "blueking" fmt.Printf("hello ")}
hello world
chan
Golang 的并发模型基于 CSP,并发实体(goroutine)通过管道(channel)进行通信。
管道本质上是一个结构体,维护发送和队列两个队列,创建管道时使用 make :ch := make(chan int, 0)
不要直接声明,这样可能会导致 goroutine 死锁:
var ch chan int
不带缓冲区的管道专用于同步模式,读写同步。
带缓冲区的管道在以下情况会阻塞:- 当缓冲区满了之后,发送者会进入发送队列等待
- 当缓冲区为空,接收者会进入接收队列等待 管道可以使用内置函数 close 关闭,关闭后的管道需要注意:
- 重复关闭会导致 panic
- 向关闭的管道发送数据会 panic
- 已关闭的管道读取数据会有以下情况:
- 先返回缓冲区的数据,直到缓冲区为空
- 直接返回类型默认值,第二个返回值是 false
- 关闭管道会退出 for … range 循环
单向管道
管道可以机上只读和只写声明:
- 只读管道: ch <-chan int
- 只写管道: ch chan<- int
这种用法一般用在函数声明中:
func handle(readCh <-chan int, writeCh chan<- int) { go func() { v := <-readCh writeCh <- 2 * v }() }goroutine
goroutine 是 Glolang 提供的一种并发模型,可以通过 关键字来启动轻量级线程来 执行指定的逻辑。 但需要注意的是,goroutine 并不是协程,底层实现是个线程池,一 个 goroutine 在执行的过程中可能会跑在不同的线程和 CPU 上。
线程安全
因为 goroutine 是在线程池中执行,因此我们在 goroutine 中访问闭包需要考虑线程安 全的问题。
Once
sync.Once 提供了一个线程安全的单次执行接口,常用于单例模式或者初始化的场景。
package mainimport ( "sync")type singleton struct { }var instance *singletonvar once sync.Oncefunc GetInstance() *singleton { once.Do(func() { instance = &singleton{ } }) return instance}
WaitGroup
Golang 没有提供类似 thread.join 等待 goroutine 结束的接口,我们可以用
sync.WaitGroup 来实现:- 初始化 WaitGroup,加上特定的值
- 激活goroutine,goroutine结束时记得调用 WaitGroup.Done()
- 主流程执行 WaitGroup.Wait()
package mainimport ( "fmt""sync")func main() { var wg sync.WaitGroup wg.Add(1) go func() { fmt.Printf("hello ") wg.Done() }() wg.Wait() fmt.Printf("world\n")}
hello world
Atomic
goroutine 可以使用闭包特性访问外部变量,或者多个 goroutine 共同修改同一个变量, 很容易陷入了变量并发访问的陷阱。 这个时候需要借助 sync.atomic 包提供的一系列 底层内存同步原语来进行同步处理。
相比于公共变量,更推荐使用管道。package mainimport ( "fmt""sync" "sync/atomic")func main() { var value int64 var wg sync.WaitGroup wg.Add(2) fun := func(count int) { for index := 0; index < count; index++ { atomic.AddInt64(&value, 1) // not value++ } wg.Done() } go fun(100) go fun(100) wg.Wait() fmt.Printf("%v\n", value)}
Mutex
sync.Mutex 提供了一个基本的互斥锁,而且不同于 Python 的 threading.RLock , 这种锁不可重入。
并且由于以下原因,估计也不会有可重入版本的锁实现出现:- Golang 故意限制了 goroutine id 的获取
- goroutine 不会固定运行在一个线程中,不能用线程 id 来做局部标识 简单来说,一个锁是没有办法知道自己被哪个 goroutine 获取的,因此这种情况下就要 尤其注意控制锁的粒度了。
- 锁住变量而不是锁住语句块
- 及时上锁及时释放 一个锁不要在粒度不同的逻辑中操作 因为 defer 声明的语句是在函数返回的时候执行的,如果这个时候进行锁的释放,显然 无形中增大了锁的控制粒度(锁住了 Lock 以后的所有语句),因此不建议这样使用。 以下程序会死锁:
package mainimport ( "sync")func main() { var lock sync.Mutex lock.Lock() lock.Lock()}
RWMutex
毫无疑问,锁对程序的性能会造成很大的影响,因此减少锁竞争时间是优化的关键。 在读操作比写操作频繁的情况下,可以用 sync.RWMutex 实现的读优先(读者不竞争)
读写锁来优化性能。打印进度示例:
package mainimport ( "fmt""sync""time")func main() { var lock sync.RWMutex var value int64 go func() { for index := 0; index < 100; index++ { time.Sleep(200 * time.Millisecond) lock.Lock() value++ lock.Unlock() } }() for { time.Sleep(300 * time.Millisecond) lock.RLock() v := value lock.RUnlock() fmt.Printf("\r%3d%%", v) if v == 100 { break } }}
Context
Context 是官方推荐的并发模式,主要用于调度 goroutine,在很多库和框架都有支持。
因为 goroutine 创建成本极低,一个请求处理的过程中往往会产生很多和这个请求相关 的 goroutine,请求处理结束或者中断后,没能及时结束的 goroutine 会泄漏, goroutine 本质上是线程,会继续占用 CPU,并且容易进一步导致内存泄漏。Context 是一种接口,相同请求范围内的 goroutine 需要主动检查 Context 的状态来进 行合适的处理:
- Done() <-chan struct{} :返回一个管道,当 Context 取消或者超时的时候会被 关闭
- Err() error :返回 Done 管道关闭的原因
- Deadline() (deadline time.Time, ok bool) :返回将要超时的时间 Value(key interface{}) interface{} :返回 Context 的值
Context 是一个独立的变量,不能保存在结构体中,需要在第一个参数以名称 ctx 主动 传递,如在 gin 中检查 Context 状态以提前结束:
func Messages(ctx *gin.Context) { messagesCh := make(chan interface{ }, 10) queryMessages(ctx, messagesCh) ctx.Stream(func(w io.Writer) bool { select { case message, ok := <-messagesCh: if ok { w.Write(MessageToBytes(message)) } return ok case <-ctx.Done(): return false } }) }
使用 Context
context.WithCancel 接收一个父 context ,并返回一个新的 context 对象和
cancel 函数。- 调用 cancel 函数将会关闭该 context ,但不影响父 context
- 父 context 关闭同时也会关闭该 context 切记不要传递 nil 作为 context ,如果不确定使用哪个具体的类型,请传递 context.TODO 。
func main() { var ( ctx, cancelFunc = context.WithCancel(context.Background()) wg sync.WaitGroup ch = make(chan int, 1) ) wg.Add(1) go func() { for { select { case value := <-ch: fmt.Printf("%v\n", value) case <-ctx.Done(): wg.Done() return } } }() for index := 0; index < 5; index++ { if index == 3 { cancelFunc() break } ch <- index } wg.Wait()}
随机数
math.rand 生成的是伪随机数,需要初始化随机数种子,且随机性不够,请使用 crypto/rand 。
crypto/rand 和 math/rand 的 Read 函数是兼容的。
项目
- 使用 来进行依赖管理
- vendor 目录要跟随项目进行版本控制,不要每次下载
编译
为了便于维护,编译使用 Makefile 来进行。
编译加速
-i 选项进行加速,相当于:
- go install
- go build 通过 pkg 目录下的 .a 文件保存中间状态。
编译优化
一些有用的 ldflags:
- -w :去掉调试信息
- -X :编译期间覆盖变量,如: -X project/config.Mode=release
在 Paas-Auth 中通过编译覆盖变量来判断当前版本是 debug 版本还是 release 版本,进 行一些差异化的配置:
var Mode = "default"func init() { if Mode != "debug" { gin.SetMode(gin.ReleaseMode) }}
通过 build 注入不同的 Mode 来设置 gin 是否开启 release 模式。
条件编译
以 _${GOOS} 命名指定编译的平台,如 xx_linux.go 仅在 linux 中编译。
Go 使用 ${GOOS} 和 ${GOARCH} 两个变量识别当前平台和架构,可以通过修改这两个 变量实现交叉编译。因此文件名请勿随便使用类似命名,以免触发 Go 默认的条件编译。
在文件顶部加上 build tag,如 gin 中关于 json 处理的文件有如下的 tag:
// +build jsoniter
如果需要启用 jsoniter 来处理 json,指定对应的 build tag 即可:
go build -tags=jsoniter
更小的可执行文件
- -w 选项:去掉调试信息
- -s 选项:去掉符号表
- 使用 upx : upx -9 bin/target
经过验证的第三方依赖
- :HTTP 框架,有中间件方案;
- :较好用的 ORM 框架,支持数据库连接池;
- :接口类似 redis-py,提供连接池;
- :陶文大神的 JSON 库;
- :结构化的日志模块
转载地址:https://blog.csdn.net/weixin_39172380/article/details/88657110 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!