这是我参与「第五届青训营 」伴学笔记创作活动的第4天
代码规范
代码格式
- 使用
gofmt自动格式化代码 - 使用
goimports格式化代码,同时也会自动增删依赖的包引用、将依赖包按字母序排序并分类
注释
注释的内容:
-
解释代码的作用
- 解释公共符号
- 解释返回值
-
解释代码如何做的
- 注释代码实现的过程
-
解释代码实现的原因
- 解释代码的外部因素
- 提供额外的上下文(注释应该提供代码未表达出的上下文信息)
-
解释代码什么情况会出错
- 解释代码的限制条件
命名规范
变量名:
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出的时候,使用全小写
- eg:
ServeHTTP而不是ServeHttp - 使用
XMLHTTPRequest
- eg:
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字需要更多的上下文信息
函数名:
- 函数名不携带包名的上下文信息,因为包名总是和函数名成对出现
- 函数名尽量简介
- 当名为foo的包的某个函数返回类型为foo时,可以省略类型信息而不导致歧义
- 当名为foo的包的某个函数返回类型为T时(T不是foo),可以在函数名中加入类型信息
包名:
- 全部小写,不使用大写和下划线
- 简短并包含一定的上下文信息
- 不与标准库同名
- 不使用常用的变量名
- 使用单数而不是复数(使用encoding而不是encodings)
- 谨慎使用缩写
控制流程
-
避免嵌套,保证正常流程清晰
-
//bad if foo { return x }else { return nil } //good if foo { return x } return nil
-
-
尽量保持正常代码路径为最小缩进
- 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
错误和异常处理
-
简单错误
简短错误是指仅出现一次的错误,且在其他地方不需要捕获该错误
优先使用
errors.New来创建匿名变量来直接表示简单错误如果有格式化需求,使用
fmt.Errorf -
复杂错误:使用Wrap和Unwrap
Wrap提供一个error嵌套到另外一个error的能力,从而生成一个error的跟踪链
在
fmt.Errorf中使用%w来将一个错误嵌套到另外一个错误链中if err!= nil { return fmt.Errorf("error from %w",err) } -
错误判定
-
判定一个错误是否为特定错误,使用
errors.Is不同于使用
==,使用该方法可以判定错误链上的所有错误是否含有特点错误data,err = lockedfile.Read(targ) if errors.Is(err,fs.ErrNotExist) { ... } -
获取特定种类的错误,使用
errors.Asif _,err:= os.Open("non-existing");err!=nil { var pathError *fs.PathError if errors.As(err,&pathError) { fmt.Println("Failed at path:",pathError.Path) } else { fmt.Println(err) } }
-
-
panic
出现不可逆转的错误,使用panic退出
-
recover
recover只能在被defer的函数中使用嵌套无法生效
只能在当前的
goroutine生效defer的语句是后进先出的
性能优化
go语言提供了支持基准性能测试的benchmark工具
性能表现需要实际的数据衡量
go test -bench=. -benchmem
Slice
-
内存预分配
//未预分配 func NoPreAlloc(size int) { data := make([]int, 0) for k := 0; k < size; k++ { data = append(data, k) } } //预分配内存 func PreAlloc(size int) { data := make([]int, 0, size) for k := 0; k < size; k++ { data = append(data, k) } }
性能差异原因:
- Slice本质是数组片段的描述(数组指针+片段长度+片段容量)
- Slice操作不复制Slice指向的元素
- 创建性的Slice会复用原来的Slice
**即预先分配内存可以不用扩容**
0. 大内存未释放
在已有的Slice基础上创建很小的Slice,此时不会创建新的Slice,此时原来的Slice得不到释放。
解决方法:使用`copy`复制创建新的Slice
```
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
```
Map
内存预分配
func BenchmarkNoPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
NoPreAlloc(1000)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
PreAlloc(1000)
}
}
String字符串
-
字符串拼接方式(
strings.Builder)//直接使用+ func Plus(n int, str string) string { s := "" for i := 0; i < n; i++ { s += str } return s } //使用strings.Builder func StrBuilder(n int, str string) string { var builder strings.Builder for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() } //使用bytes.Buffer func ByteBuffer(n int, str string) string { buf := new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(str) } return buf.String() } //使用strings.Builder预分配内存 func PreStrBuilder(n int, str string) string { var builder strings.Builder builder.Grow(n * len(str)) for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() } //使用bytes.Buffer预分配内存 func PreByteBuffer(n int, str string) string { buf := new(bytes.Buffer) buf.Grow(n * len(str)) for i := 0; i < n; i++ { buf.WriteString(str) } return buf.String() } 性能差异原因:
string是不可变类型,每次拼接的都要重新分配内存bytes.Buffer转为字符串会重新申请空间,strings.Builder直接将底层[]byte转为stringstrings.Builder和bytes.Buffer底层都是[]byte数组,根据内存扩容策略使得不需要每次拼接都扩容,所以预分配内存会更快
Struct 结构体
-
空结构体不占据任何内存空间,在某些场景下可以单做占位符来使用。
可以用来实现
setfunc EmptyStructMap(n int) { m := make(map[int]struct{}) for i := 0; i < n; i++ { m[i] = struct{}{} } } //设置为bool会多占据一个字节 func BoolMap(n int) { m := make(map[int]bool) for i := 0; i < n; i++ { m[i] = false } }
多线程使用atomic
使用atomic包自动实现变量的原子性,比使用加锁来实现效率高很多。
package benchatomic
import (
"sync"
"sync/atomic"
)
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
type mutexCounter struct {
i int32
m sync.Mutex
}
func MutexAddOne(c *mutexCounter) {
c.m.Lock()
c.i++
c.m.Unlock()
}
原因:
- 锁的实现是操作系统的系统调用来实现的,atomic操作是通过硬件实现,效率更高
sync.Mutex应该用来保护一段逻辑,保护变量使用atomic- 非数值操作可以使用
atomic.Value,能承载一个interface{}
性能优化实践
- 依靠数据而不是猜测
- 定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
使用分析工具pprof
- 知道在什么地方耗费了多少CPU、内存
- 可视化
flat == Cum没有调用其他函数
flat == 0函数中只调用了其他函数且没什么消耗
定位到消耗最大的函数是Eat
使用list命令来定位:list Eat
命令web可视化调用关系