关于程序员:浅谈-golang-代码规范-性能优化和需要注意的坑

31次阅读

共计 8657 个字符,预计需要花费 22 分钟才能阅读完成。

浅谈 golang 代码标准, 性能优化和须要留神的坑

编码标准

[强制] 申明 slice

申明 slice 最好应用

var t []int 

而不是应用

t := make([]int, 0)

因为 var 并没有初始化,然而 make 初始化了。

然而如果要指定 slice 的长度或者 cap,能够应用 make

最小作用域

if err := DoSomething(); err != nil {return err}

尽量减少作用域, GC 比拟敌对

赋值标准

申明一个对象有 4 种形式:make, new(), var, :=

比方:

t := make([]int, 0)
u := new(User)
var t []int
u := &User{}
  • var 申明然而不立即初始化
  • := 申明并立即应用
  • 尽量减少应用 new() 因为他不会初始化值, 应用 u := User{} 更好

接口命名

单个性能应用 er 结尾或者名词

type Reader interface {Read(p []byte) (n int, err error)
}

2 个性能

type ReaderWriter interface {
    Reader
    Writer
}

3 个及以上性能

type Car interface {Drive()
    Stop()
    Recover()}

命名标准

代码格调

[强制] go 文件应用下划线命名

[强制] 常量应用下划线或者驼峰命名, 表白革除不要嫌名字太长

[举荐] 不要在名字中携带类型信息

// 反例
userMap := map[string]User{}
// 正例
users := map[string]User{}

[举荐] 办法的参数要能表白含意

// 反例
func CopyFile(a, b string) error 
// 正例
func CopyFile(src, dst string) error 

[举荐] 包名一律应用小写字母, 不要加下划线或者中划线

[举荐] 如果应用了设计模式, 名称中体现设计模式的含意

type AppFactory interface {CreateApp() App
}

[举荐] 如果变量名是 bool 类型, 如果字段名不能表白 bool 类型, 能够应用 is 或者 has 前缀

var isDpdk bool

[强制] 一个变量只能有一个性能, 并和名称统一, 不要把一个变量作为多种用处

// 反例
length := len(userList)
length := len(orderList)
// 正例
userNum := len(userList)
orderNum := len(orderList)

[举荐] 如果变量名是 bool 类型, 如果字段名不能表白 bool 类型, 能够应用 is 或者 has 前缀

golang 根本标准

包设计

[强制] 包的设计满足繁多职责

阐明: 在 SRP (Single Response Principle) 模式中, 繁多职责准则是指一个类只负责一项性能, 并且不能负责多个性能. 将包设计的十分内聚, 缩小包之间的 api

[强制] 包的设计遵循最小可见性准则

阐明: 仅在包内调用的函数, 或者变量, 或者构造体, 或者接口, 或者类型, 或者函数等等, 须要小写结尾, 不能够能够被内部包拜访

[强制] 代码须要可测试性, 应用接口和依赖注入代替硬编码

[强制] 单元测试文件放到代码文件同级目录, 便于 golang 工具应用

比方: vscode 在办法上右键能够间接生成测试代码和测试覆盖率并可视化展现执行状况

布局

[举荐] 程序实体之间应用空行辨别, 减少可读性

阐明: 比方函数中各个模块性能应用空行辨别, 减少可读性

[举荐] 每个文件开端应该有且仅有一个空行

[举荐] 一元操作符不要加空格, 二元操作符的才须要

正文

[举荐] 可导出的办法, 变量, 构造体等都须要正文

表达式和语句

[举荐] if 或者循环的嵌套层数不宜大于 3

[举荐] 对于 for 遍历, 优先应用 range 而不是显式 ’ 的下标, 如果 value 占用内存大的话能够应用显式下标

阐明: range 能够让代码更加整洁, 特地是多层 for 嵌套的时候, 然而 range 非拷贝值, 如果 value 不是指针类型, 而且占用内存较大会有性能损耗.

函数

[强制] 命名不要裸露实现细节, 个别以 ” 做什么 ” 来命名而不是 ” 怎么做 ”

[举荐] 短小精悍, 尽量管制到 20 行左右

阐明: 函数的粒度越小, 可复用的概率越大, 而且函数越多, 高层函数调用起来就是代码可读性很高, 读起来就像一系列解释

[举荐] 繁多职责, 函数只做好一件事件, 只做一件事件

[强制] 不要设置多功能函数

例如: 一个函数既批改了状态, 又返回了状态, 应该拆分

[举荐] 为简略的性能编写函数

阐明: 为 1,2 行代码编写函数也是必要的, 减少代码的可复用性, 减少高层函数的可读性, 可维护性, 可测试性

参数

[举荐] 参数个数限度在 3 个以内, 如果超过了, 能够应用配置类或者 Options 设计模式

阐明: 函数的最现实的参数的个数首先是 0, 而后是 1, 而后是 2, 3 就会很差了. 因为参数有很多概念性, 可读性差, 而且让测试十分复杂

[举荐] 函数不能含有示意参数

阐明: 标识参数俊俏不堪, 函数往往依据标识参数走不同的逻辑, 这个和繁多职责违反

[强制] struct 作为参数传递的时候, 应用指针

阐明: 函数的执行就是压栈, struct 如果有多个字段将会被屡次压栈, 有性能损失, 指针只会被压栈一次

[举荐] 在 api(controller) 层对传入的参数进行查看, 而不是每一层都查看一次

[举荐] 当 chan 作为函数的参数的时候, 依据最小权限准则, 应用单向 chan

// 读取单向 chan
func Parse(ch <-chan struct{}) {
    for v := range ch {println(v)
    }
}

// 写入单向 chan
func Do(down chan<- struct{}) {time.Sleep(time.Second)
    down <- struct{}{}
}

返回值

[举荐] 返回值的个数不要大于 3

[强制] 对立定义谬误, 不要轻易抛出谬误

阐明: 比方记录不存在可能有多种谬误

"record not exits"
"record not exited"
"record not exited!!"

下层函数要解决底层的谬误的话, 要晓得所有的抛出状况, 这个是不事实的, 须要解决的谬误应该应用对立文件定义错误码

[强制] 没有失败起因的时候, 不要应用 error

// 正例
func IsPhone() bool 
// 反例
func IsPhone() error

[举荐] 当多重试几次能够防止失败的时候, 不要返回 error

谬误是偶尔产生的, 应该给一个机会重试, 能够防止大多数的偶尔问题

[举荐] 下层函数不关怀 error 的时候, 不要返回 error

比方 Close(), Clear() 抛出了 error, 下层函数大概率不晓得怎么解决

异样设计

[举荐] 程序的开发阶段, 保持速错, 让异样程序解体

阐明: 速错的实质逻辑就是 “ 让它挂 ”, 只有挂了你才第一工夫晓得谬误, panic 能让 bug 尽快被修复

[强制] 程序部署后, 应该防止终止

是否 recover 应该依据配置文件确定, 默认须要 recover

留神: 有时候须要在提早函数中开释资源, 比方 panic 之前 read 了 channel, 然而还没有 write 就 panic , 须要在 deffer 函数中做好解决, 避免 channel 阻塞.

[举荐] 当入参不非法的时候, panic

阐明: 当入参不非法的时候, panic, 能够让下层函数晓得谬误, 而不是继续执行(api 应该提前做好参数查看)

整洁测试

[强制] 不要为了测试对代码进行入侵式的批改, 应该 mock

阐明: 禁止为了测试在函数中减少条件分支和测试变量

[举荐] 测试的三要数, 可读性, 可读性, 可读性

生产代码的可靠性由测试代码来保障, 测试代码的可靠性由最简略的可读性来保障, 逻辑须要简略到没有 bug

REFERENCE

bilibili go 标准

uber go-guide https://github.com/xxjwxc/ube…

golang 性能优化

内存优化

小对象合并

小对象在堆内存上频繁的创立和销毁, 会导致内存碎片, 个别会才应用内存池

golang 的内存机制也是内存池, 每个 span 大小为 4KB, 同时保护一个 cache, cache 有一个 list 数组

数组外面贮存的是链表, 就像 HashMap 的拉链法, 数组的每个格子代表的内存大小是不一样的, 64 位的机器是 8 byte 为根底, 比方下标 0 是 8 byte 大小的链表节点, 下标 1 是 16 byte 的链表节点, 每个下标的内存不一样, 应用的是按需分配最近的内存, 比方一个构造体的内存实际上算下来是 31 byte, 调配的时候会调配 32 byte.

一个下标的一条链表的每个 Node 贮存的内存是统一的.

所以倡议将小对象合并为一个 struct

for k, v := range m {x := struct {k , v string} {k, v} // copy for capturing by the goroutine
    go func() {// using x.k & x.v}()}

应用 buf 缓存

协定编码的时候须要频繁的操作 buf, 能够应用 bytes.Buffer 作为缓存区对象, 它会一次性调配足够大的内存, 防止内存不够的时候动静申请内存, 缩小内存调配次数, 而且, buf 能够被复用(倡议复用)

slice 和 map 创立的时候, 预估大小指定的容量

事后分配内存, 能够缩小动静扩容带来的开销

t := make([]int, 0, 100)
m := make(map[string]int, 100)

如果不确定 slice 会不会初始化, 应用 var 这样不会分配内存, make([]int,0) 会分配内存空间

var t []int

拓展:

slice 容量在 1024 前扩容是倍增, 1024 后是 1 /4

map 的扩容机制比较复杂, 每次扩容是 2 倍数, 构造体中有一个 bucket 和 oldBuckets 实现增量扩容

长调用栈防止申请较多的长期对象

阐明: goroutine 默认的 栈的大小是 4K, 1.7 改为 2K, 它采纳的是间断栈的机制, 当栈空间不够的时候, goroutine 会一直扩容, 每次扩容就先 slice 的扩容一样, 设计新的栈空间申请和旧栈空间的拷贝, 如果 GC 发现当初的空间只有之前的 1/4 又会缩容, 频繁的内存申请和拷贝会带来开销

倡议: 管制函数调用栈帧的复杂度, 防止创立过多的长期对象, 如果的确须要比拟长的调用栈或者 job 类型的代码, 能够思考将 goroutine 池化

防止频繁创立长期变量

阐明: GC STW 的工夫曾经优化到最蹩脚 1ms 内了, 然而还是有混合写屏障会升高性能, 如果长期变量个数太多, GC 性能损耗就高.

倡议: 升高变量的作用域, 应用局部变量, 最小可见性, 将多个变量合并为一个 struct 数组(升高扫描次数)

大的 struct 应用指针传递

golang 都是值拷贝, 特地是 struct 入栈帧的时候会将变量一个一个入栈, 频繁申请内存, 能够应用指针传递来优化性能

并发优化

goroutine 池化

go 尽管轻量, 然而对于高并发的轻量级工作, 比方高并发的 job 类型的代码, 能够思考应用 goroutine 池化, 缩小 goroutine 的创立和销毁, 缩小 goroutine 的创立和销毁的开销

缩小零碎调用

goroutine 的实现是通过同步模仿异步操作, 比方上面的操作并不会阻塞, runtime 的线程调度

  • 网络 IO
  • channel
  • time.Sleep
  • 基于底层异步的 SysCall

上面的阻塞会创立新的线程调度

  • 本地 IO
  • 基于底层同步的 SysCall
  • CGO 调用 IO 或者其余阻塞

倡议将同步调用: 隔离到可控 goroutine 中, 而不是间接高并 goroutine 调用

缩小锁, 缩小大锁

Go 举荐应用 channel 的形式调用而不是共享内存, channel 之间存在大锁, 能够将锁的力度升高

拓展: channel

channel 不要传递大数据, 会有值拷贝

channel 的底层是链表 + 锁

不要用 channel 传递图片等数据, 任何的队列的性能都很低, 能够尝试指针优化大对象

合并申请 singleflight

参考: singleflight

协定压缩 protobuf

protobuf 比 json 的贮存效率和解析效率更高, 举荐在长久化或者数据传输的时候应用 protobuf 代替 json

批量协定

对数据拜访接口提供批量协定, 比方门面设计模式或者 pipeline, 能够缩小十分多的 IO, QPS, 和拆包解包的开销

并行申请 errgroup

对于网关接口, 通常须要聚合多个模块的数据, 当这些业务模块数据之间没有依赖的时候, 能够并行申请, 缩小耗时

ctxTimeout, cf := context.WithTimeout(context.Background(), time.Second)
defer cf()
g, ctx := errgroup.WithContext(ctxTimeout)
var urls = []string{
    "http://www.golang.org/",
    "http://www.google.com/",
    "http://www.somestupidname.com/",
}
for _, url := range urls {
    // Launch a goroutine to fetch the URL.
    url := url // https://golang.org/doc/faq#closures_and_goroutines
    g.Go(func() error {
        // Fetch the URL.
        resp, err := http.Get(url)
        if err == nil {resp.Body.Close()
        }
        return err
    })
}
// Wait for all HTTP fetches to complete.
if err := g.Wait(); err == nil {fmt.Println("Successfully fetched all URLs.")
}
select {case <-ctx.Done():
    fmt.Println("Context canceled")
default:
    fmt.Println("Context not canceled")
}

其余优化

须要留神的坑

channel 之坑

如何优雅的敞开 channel

参考: 如何优雅的敞开 channel

敞开 channel 的坑
  • 敞开曾经敞开的 channel 会导致 panic
  • 给敞开的 channel 发送数据会导致 panic
  • 从敞开的 channel 中读取数据是初始值默认值
CCP 准则

CCP: Channel Close Principle (敞开通道准则)

  • 不要从接收端敞开 channel
  • 不要敞开有多个发送端的 channel
  • 当发送端只有一个且前面不会再发送数据才能够敞开 channel
有缓存的 channel 不肯定有序

defer 之坑

defer 中的变量

参数传递是在调用的时候

i := 1
defer println("defer", i)
i++
// defer 1

非参数的闭包

i := 1
defer func() {println("defer", i)
}()
i++
// defer 2

有名返回同理闭包, 并且会批改有名返回的返回值

func main(){fmt.Printf("main: %v\n", getNum())
    // defer 2
    // main: 2
}

func getNum() (i int) {defer func() {
        i++
        println("defer", i)
    }()
    i++
    return
}

不要 for 循环中调用 deffer

因为 deffer 只会在函数 return 之后执行, 这样会累积大量的 deffer 而且极其容易出错

倡议: 将 for 循环须要 deffer 的代码逻辑封装为一个函数

HTTP 之坑

request 超时工夫

golang 的 http 默认的 request 没有超时, 这是一个大坑, 因为如果服务器没有响应, 也没有断开, 客户端会始终期待, 导致客户端阻塞, 量一上来就解体了

敞开 HTTP 的 response

http 申请框架的 response 肯定要通过 Close 办法敞开, 不然有可能内存泄露

interface 之坑

interface 到底什么才等于 nil?

阐明: interface{}和接口类型 不同于 struct, 接口底层有 2 个成员, 一个是 type 一个是 value, 只有当 type 和 value 都为 nil 时, interface{} 才等于 nil

var u interface{} = (*interface{})(nil)
if u == nil {t.Log("u is nil")
} else {t.Log("u is not nil")
}
// u is not nil

接口

var u Car = (Car)(nil)
if u == nil {t.Log("u is nil")
} else {t.Log("u is not nil")
}
// u is nil

自定义的 struct

var u *user = (*user)(nil)
if u == nil {t.Log("u is nil")
} else {t.Log("u is not nil")
}
// u is nil

map 之坑

map 并发读写

map 并发读写会 panic, 须要加锁或者应用 sync.Map

map 不能间接更新 value 的某一个字段

type User struct{name string}
func TestMap(t *testing.T) {m := make(map[string]User)
    m["1"] = User{name:"1"}
    m["1"].name = "2"
    // 编译失败,不能间接批改 map 的一个字段值
}

须要独自拿进去

func TestMap(t *testing.T) {m := make(map[string]User)
    m["1"] = User{name: "1"}
    u1 := m["1"]
    u1.name = "2"
}

切片之坑

数组是值类型, 切片是援用类型(指针)

func TestArray(t *testing.T) {a := [1]int{}
    setArray(a)
    println(a[0])
    // 0
}
func setArray(a [1]int) {a[0] = 1
}
func TestSlice(t *testing.T) {a := []int{1,}
    setSlice(a)
    println(a[0])
    // 1
}
func setSlice(a []int) {a[0] = 1
}

range 遍历

range 会给每一个元素创立一个正本, 会有值拷贝, 如果数组存的是大的构造体能够用 index 遍历或者指针优化

因为 value 是正本, 所以不能批改原有的值

append 会扭转地址

slice 类型的实质是一个构造体

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

函数的值拷贝会导致批改生效

func TestAppend1(t *testing.T) {var a []int
    add(a)
    println(len(a))
    // 0
}

func add(a []int) {a = append(a, 1)
}

闭包之坑

并发下 go 函数闭包问题

for i := 0; i < 3; i++ {go func() {println(i)
    }()}
time.Sleep(time.Second)
// 2
// 2
// 2

阐明: 因为闭包导致 i 变量逃逸到堆空间, 所有的 go 共用了 i 变量, 导致并发问题

解决办法 1: 局部变量

for i := 0; i < 3; i++ {
    ii := i
    go func() {println(ii)
    }()}
time.Sleep(time.Second)
// 2
// 0
// 1

解决办法 2: 参数传递

for i := 0; i < 3; i++ {go func(ii int) {println(ii)
    }(i)
}
time.Sleep(time.Second)
// 2
// 0
// 1

buffer 之坑

buffer 对象池

buffer 对象池肯定要用完才还回去, 不然 buffer 在多处复用导致底层的 []byte 内容不统一

参考: golang-buffer-pool

咱们的一个 httpClient 返回解决应用了 sync.pool 缓存 buffer, 测试是内存优化了 6 - 8 倍

前面测试的时候发现, 获取的内容会偶然不统一, review 代码发现可能是并发时候 buffer 指针放回去了还在应用, 导致和 buffer pool 外面不统一
首先思考就是将 buffer 的 bytes 读取进去, 而后再 put 回池子外面
而后 bytes 是一个切片, 底层还是和 buffer 共用一个 []byte, buffer 再次批改的时候底层的 []byte 也会被批改, 导致状态不统一
这些实践上是并发问题, 然而咱们测试发现, 单线程调用 httpClient 时候, 有时候会有问题, 有时候又没有问题
官网的 http client 做申请的时候会开一个协程, sync pool 在同一个协程上面复用对象是统一的, 然而多协程就会新建, 会尝试通过协程的 id 获取与之对应的对象, 没有才去新建.
串行执行申请也会产生多个协程, 所以偶然会触发新建 sync 的 buffer, 如果新建就不会报错, 如果不新建就会报错.

select 之坑

for select default 之坑

for 中的 default 在 select 肯定会执行, CPU 始终被占用不会让出, 导致 CPU 空转

示例代码

func TestForSelect(t *testing.T) {
    for {
        select {case <-time.After(time.Second * 1):
            println("hello")
        default:
            if math.Pow10(100) == math.Pow(10, 100) {println("equal")
            }
        }
    }
}

top CPU 跑满了

top - 15:00:50 up 1 day, 15:55,  0 users,  load average: 1.36, 0.85, 0.35
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND   
28632 root      20   0 2168296   1.4g   2244 S 252.8  11.7   1:04.15 __debug_bin   

正文完
 0