GO开发规范
发布日期:2021-11-18 19:17:13 浏览次数:8 分类:技术文章

本文共 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 的方式命名
    • ReadReader
    • WriteWriter

注释

语法

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 , 如用户参数校验

失败,数据库查找失败。

  1. 在 package 中新建一个 errors.go,导出依赖或函数的错误;
  2. 函数中不再直接 return errors.New(“xxx”) ,因为错误是 Immutable 的,直接
    使用全局对象;
  3. 使用 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 关闭,关闭后的管道需要注意:
  1. 重复关闭会导致 panic
  2. 向关闭的管道发送数据会 panic
  3. 已关闭的管道读取数据会有以下情况:
    1. 先返回缓冲区的数据,直到缓冲区为空
    2. 直接返回类型默认值,第二个返回值是 false
  4. 关闭管道会退出 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 来实现:

  1. 初始化 WaitGroup,加上特定的值
  2. 激活goroutine,goroutine结束时记得调用 WaitGroup.Done()
  3. 主流程执行 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 , 这种锁不可重入。

并且由于以下原因,估计也不会有可重入版本的锁实现出现:

  1. Golang 故意限制了 goroutine id 的获取
  2. 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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:GO单元测试
下一篇:Mac 使用git出现xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools)

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月19日 04时15分33秒