传说中的go语言圣经,得看一看(•̀⌄•́)
                              —— By Jihan
主题:GO语言学习
类别:计算机->实用性+理论性论述书->golang
概要:从语法,特性,接口,工具,测试等方面系统性介绍go语言。总体来说是针对语言本身特性来进行介绍的。
前言
最近在设计一个项目的框架,所使用的就是golang语言,期望多看看书以及学习一些源码,从而有更优雅的架构设计。
书中有许多的练习程序,手动写代码,跑程序也是很重要的。
主要内容
太过于基础的语法,并不会在笔记中出现,这里主要记录一些go语言编写中需要注意的点。
基础语法
主要包含一些语法和关键语句特性相关内容。
- 
如果传参使用 map或者slice实际传递的是引用的拷贝,也就是指针的拷贝,那么函数里对map的操作对调用者来说也是可见的。比如下面的代码:1 
 2
 3func insertMap(m map[int]string){ 
 m[1] = "1"
 }
- 
switch中的值会和每一个case进行比较,直到找到匹配的值。因此不要在case中放耗时的操作。select也是同样如此。
- 
短变量声明一般用作局部变量中。示例: 1 
 2
 3in,err := os.Open(file)//声明两个变量in,err 
 out,err := os.Open(out)//声明变量out,赋值变量err
 out,err := os.Open(out1)//报语法错误
- 
多重赋值的时候,会先完全计算完右边的值,再赋值给左边。 1 x,y = x-y, x+y//可以完全得到预想的值,即x=x[旧]-y[旧], y=x[旧]+y[旧] 
- 
字符串 string是不可变字节序列,不可越界,不可修改,运算只能产生新的变量,而非修改原有值。
- 
无类型常量 const往往拥有更高的精度和更广的取值范围,并且能够适用于很多地方。
- 
数组的长度也是数组类型的一部分。 - 数组初始化:
 1 
 2
 3q := [...]int{1,2}//同r类型,...会自己根据初始化数据个数填入 
 r := [2]int{1,2}
 c := [...]int{99:-1}//len(c)=100, 除去c[99] = -1,其他都是0
- 
数组直接传递,是值传递,不同其他语言是引用传递。如果想使用引用传递,需要定义成指针类型: func zero(a *[32]int)
- 
slice为空的判断,使用len(s) == 0而不是s == nil
- 
go语言有可变长度的栈,最大可以达到1G,能更加安全的使用递归 
- 
defer函数的实际执行顺序以调用顺序的倒叙执行。- 当发生panic时,所有defer也将以倒叙执行。
 
- 当发生
- 
内置函数 recover只能在defer中调用,并且当发生panic时能终止当前的宕机,并且从发生panic的位置正常返回(不会往下执行)。- 通常情况下,不建议在recover中进行程序恢复,这样更容易掩盖程序中的bug
- 常用recover的方式是提供一些更加有用的信息。
 
- 通常情况下,不建议在
函数及变量作用域
- 
作用域: - 整个程序级别:int,new等内置类型或函数
- 包级别:同一个包的任何地方引用,比如包里的全局变量
- 文件级别:只有导入了该包才能使用的。比如fmt.Print
- 块儿级别:函数内部,循环内部等。
 
- 整个程序级别:
- 
函数作为参数的时候,传递的作用域问题,本质上函数参数传递的是指针,那么使用的时候也是直接使用指针指向的函数,只要指针本身有效(不是野指针),函数都能正常执行: 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22func runFunc() { 
 s := SendVar{
 1,
 2,
 }
 func_scope.CallBack(func(i int) error {
 fmt.Println("inner call back func", s.inner, s.Out)
 return nil
 })
 func_scope.CallBack(print)
 func_scope.CallBack(s.print) //这里如果,s对象不存在了过后,容易导致函数指针读取错误。导致panic
 }
 func (s SendVar) print(i int) error {
 fmt.Println("call back print func", s.inner, s.Out)
 return nil
 }
 func print(i int) error {
 fmt.Println("call back print func", i)
 return nil
 }
- 
同第三点所说,当结构体作为参数的时候,其他包是否有权限读取内部变量。类似json这种结构体中的内部变量就不会进行转换。实际上,直接读取肯定是无法读取的,写代码都编译不过。但可以通过反射获取,示例: 1 
 2
 3
 4func main(){ 
 vs := var_scope.VS{Out: 1}
 fmt.Println("Print var_scope VS", vs)
 }var_scope包: 1 
 2
 3
 4type VS struct { 
 Out int
 inner int
 }那么这种情况会输出: Print var_scope VS {1 0},意思是fmt.Println可以读取到包内部变量。实际看源码我们发现,在fmt.Println中实际使用reflect.Value.Int()函数直接获取值(其他类型有对应转换函数),是能获取到内部变量的,如果使用reflect.Value.Interface()来获取值,就会进行安全检查:1 
 2
 3
 4
 5
 6if safe && v.flag&flagRO != 0 { 
 // Do not allow access to unexported values via Interface,
 // because they might be pointers that should not be
 // writable or methods or function that should not be callable.
 panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
 }那么,我们看下面的示例: 
 var_scope包1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14func PrintOtherStruct(data interface{}) { 
 val := reflect.ValueOf(data)
 typeOfTstObj := val.Type()
 for i := 0; i < val.NumField(); i++ {
 fieldType := val.Field(i)
 //fieldType.Interface() 函数提示不能返回域外的值的panic,可以通过CanInterface函数来判断
 //fieldType.Int() 直接转换成对应类型的值,不会检测。
 fmt.Printf("object field %d key=%s value=%v type=%s \n",
 i, typeOfTstObj.Field(i).Name, fieldType.Int(),
 fieldType.Type())
 }
 fmt.Println("var scope print", data)
 }1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13type SendVar struct { 
 Out int
 inner int
 }
 func main() {
 s := SendVar{
 1,
 2,
 }
 var_scope.PrintOtherStruct(s)
 }
- 
匿名函数,func关键字后没有变量的函数,例如: strings.Map(func(r rune) rune{return r + 1}, "HAL-9000")。匿名函数有一些特性- 匿名函数能够获取到整个词法环境,里层能够使用外层的变量。
 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19func squares() func() int { 
 var x int
 return func() int {
 x++
 return x * x
 }
 }
 func main() {
 f := squares()
 fmt.Println(f()) //1
 fmt.Println(f()) //4
 fmt.Println(f()) //9
 f2 := squares()
 fmt.Println(f2()) //1
 fmt.Println(f2()) //4
 fmt.Println(f2()) //9
 }这里的匿名函数是能够使用外层的变量x的。并且变量x在调用 squares返回后亦然存在(虽然是隐藏在变量f中,有点像var x int; func()int{...}整个一体为f变量一样)。
 2. 匿名函数作为递归,必须先申明再赋值,直接声明赋值会导致未定义错误:1 
 2
 3
 4
 5visitAll := func(items []string){ 
 //...
 visitAll(m[item]) //compile error: undefind visitAll
 //...
 }- 匿名函数在for循环中的外部变量引用,先看示例:
 1 
 2
 3
 4
 5
 6
 7
 8
 9var rmdirs []func() 
 dirs := tmpdirs()
 for i:=0;i<len(dirs);i++{
 //a := i //后面os.RemoveAll(dirs[i])替换为os.RemoveAll(dirs[a])才正确
 os.MkdirAll(dirs[i],0755)
 rmdirs = append(rmdirs, func(){
 os.RemoveAll(dirs[i])//不正确
 })
 }在循环里(指 for开始到{中间)创建的变量,都是共享相同的变量,意味着匿名函数在使用的时候都是指向的一个地方。而如果内部声明的变量,如a:=i里的a则每次都会声明一个变量,隐藏在匿名函数中,而不会释放。
- 
指针类型成员函数,使用非指针类型成员也能正常调用,golang会自动对指针和指针指向的值进行转换: 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25package main 
 import (
 "fmt"
 )
 type A struct{
 Num int
 }
 type B struct{
 A
 }
 func (a *A)set(num int){
 a.Num = num
 }
 func main() {
 a := A{}
 a.set(222)
 fmt.Printf("A:%v\n",a)
 b := B{}
 b.set(11)
 fmt.Printf("B:%v\n",b)
 }输出: 1 
 2A:{222} 
 B:{{11}}
接口和反射
- 
nil也可以作为类方法的接收者,比如var a *NewStruct= (* NewStruct)(nil),但是通常不会这样使用。
- 
接口就是提取一系列具体类型的共性而构成的。很多的常用系统函数都是提供接口值传递,支持自定义的类型传递(实现接口就行)。 
- 
接口值由两部分组成:类型和动态值。 - 如果动态值可以比较(不是map或者slice这类),那么接口值也是可以比较的。
- var w *io.writer = os.Stdout。接口值里的类型是- *os.File,对应值是- &os.File{fd=1}
- 注意含有空指针的非空接口:
 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10func main(){ 
 var a *os.File
 check(a)
 }
 func check(w *io.writer){
 //注意,这个地方传入的w,其接口值不为空。因为接口值存在非空类型值`*os.File`
 if w == nil {
 panic("w is nil")
 }
 }
- 
error是一个接口,定义如下:1 
 2
 3type error interface{ 
 Error() string
 }
- 
断言: f, ok := x.(T)
- 
有些函数包可以通过断言 error的类型来判断错误类型。
- 
可以通过断言来判断某个接口里是否包含某个方法。 1 
 2
 3
 4
 5
 6
 7
 8
 9func writeString(w *io.Writer, s string)(int, error){ 
 type stringWriter interface{
 WriteString(string)(int, error)
 }
 if sw, ok := w.(WriteString); ok{
 return sw.WriteString(s)
 }
 ....
 }
- 
反射 reflect- 反射由两部分构成:Type和Value。对应着interface的类型值和类型值(实际数据)
 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
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73//一个反射的示例 
 package goreflectexample
 import (
 "fmt"
 "reflect"
 "strconv"
 )
 func formatAtom(v reflect.Value) string {
 switch v.Kind() {
 case reflect.Invalid:
 return "Invalid"
 case reflect.Int, reflect.Int8, reflect.Int16,
 reflect.Int32, reflect.Int64:
 return strconv.FormatInt(v.Int(), 10)
 //....简化起见,省略浮点数和复数
 case reflect.Bool:
 return strconv.FormatBool(v.Bool())
 case reflect.String:
 return v.String()
 case reflect.Chan, reflect.Func, reflect.Ptr,
 reflect.Slice, reflect.Map:
 return v.Type().String() + " 0x" +
 strconv.FormatUint(uint64(v.Pointer()), 16)
 default: //reflect.Array, reflect.Struct, reflect.Interface
 return v.Type().String() + " value"
 }
 }
 func display(path string, v reflect.Value) {
 switch v.Kind() {
 case reflect.Invalid:
 fmt.Printf("%s = Invalid\n", path)
 case reflect.Slice, reflect.Array:
 for i := 0; i < v.Len(); i++ {
 display(fmt.Sprintf("%s[%d]\n", path, i), v.Index(i))
 }
 case reflect.Struct:
 for i := 0; i < v.NumField(); i++ {
 feildPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
 display(feildPath, v.Field(i))
 }
 case reflect.Map:
 // 另外一个遍历map的方式
 // iter := v.MapRange()
 // for iter.Next() {
 // k := iter.Key()
 // va := iter.Value()
 // display(fmt.Sprintf("%s[%s]\n", path, formatAtom(k)), va)
 // }
 for _, key := range v.MapKeys() {
 display(fmt.Sprintf("%s[%s]\n", path, formatAtom(key)), v.MapIndex(key))
 }
 case reflect.Ptr:
 if v.IsNil() {
 fmt.Printf("%s = Nil\n", path)
 } else {
 fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
 display(path+".value", v.Elem())
 }
 default: // 基本类型、通道、函数
 fmt.Printf("%s = %s\n", path, formatAtom(v))
 }
 }
 func Display(name string, x interface{}) {
 fmt.Printf("Display %s (%T)\n", name, x)
 display(name, reflect.ValueOf(x))
 }当然,上述函数还有优化点,对于多处字符串 +的操作,使用bytes.Buffer则更能提升性能。
 2. 我们可以通过(Value).Set函数来对可寻址的变量进行赋值。是否可寻址(获取变量指针)可以用(Value).CanAddr()来确定。当然也可以通过取地址+断言的方式来获取指针,进行赋值:px := x.Addr().Interface().(*int); *px=3
 3. 反射可以获取到未导出字段的值,但是不能通过反射来更新这些值。
 4. 可以通过反射获取结构体字段标签比如json中使用的json:"name"可以通过reflect.StructField.Tag.Get("json")来获取相应的值。
 5. 可以通过反射获取相应方法:(Value).Method(i),函数数量可以通过(Value).NumMethod()来获取。
- 
使用反射的注意事项(总结:没有必要用反射,就不要用反射): - 反射代码是很脆弱的,很容易出bug,并且bug是在遇到对应类型的时候以panic的方式抛出。
- 反射无法做静态类型检测,特别是interface{}或reflect.Value参数时,一定要说明参数访问和其他限制。并且大量使用反射也不便于理解。
- 使用反射是比特定类型优化的函数低一两个数量级。有些关键性的核心函数,还是要避免使用反射。
 
- 反射代码是很脆弱的,很容易出bug,并且bug是在遇到对应类型的时候以
- 
unsafe包中包含了许多接近底层的一些操作,当然这些操作在C语言里面可能是常见的。如果不是很有必要,并不建议使用unsafe包,unsafe包会导致更不易察觉的错误,以及削弱代码可移植性。- unsafe.Pointer指向一个- T类型的指针,类似C里的- void *。- unsafe.Pointer可以转换为- uintptr,然后对指针进行数值计算。但是注意如果使用- uintptr作为中间变量,由于其类型为一个数据,那么其对应的值所指向的值可能被垃圾回收移动或者回收,导致不易发现的错误。如:
 1 pT := uintptr(unsafe.Poniter(new(T))) //语句执行后,创建的T就要被回收,pT值就变成野指针。 如果要使用uintptr,需要谨慎使用,保证使用的最小范围。 
- 
reflect.DeepEqual深度比较,会进行两个任意数据的类型,长度,和数据的比较。
- 
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P直接辨识的:- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接口接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接口接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址
 
goroutine和通道
- 
协程之间是没有父子关系的,所有子协程都是一个等级,挂在main函数下。 - 父协程退出,子协程不会退出
- 函数退出,函数内的协程不会退出(main函数除外)
 
- 
管道类型 chan T和map类似,传递都是进行指针传递。1 
 2
 3
 4x, ok := <- ch 
 if !ok {
 fmt.Fatal("channel closed.")
 }- 管道可以手动关闭(也可以让gc自己回收),关闭后任何向管道里写入的操作都会崩溃,读取操作能够正常执行,读取完管道值后再读取的值就会零值。
- 单向通道:类型chan<- T只能发送的的通道,类型<-chan T只能接收的通道。类型chan T传值给单向通道的时候,都会进行隐式转换到相应的结构。
- 带有缓冲的通道,通道的数据存取类似于管道,先进先出。如果粗暴的在一个goroutine中将管道作为队列使用,可能会出现永久阻塞的风险。
- 在select中,如果通道是nil,则改分支永远不会被选中,该特性可以用来启用或禁用某些特性。
- 可以在goroutine中使用管道作为令牌桶,控制goroutine的并发量:
 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12var tokens := make(chan struct{}, 10) 
 for i:=0;i<1000;i++{
 go func(){
 select {
 case tokens <- struct{}: //获取令牌
 case <-done: //用于控制进程取消操作。
 return nil
 }
 defer func(){ <- tokens }() //释放令牌
 //do something
 }()
 }
- 
不要通过共享内存来通信,应该使用通信来共享内存。 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22package bank 
 var deposits = make(chan int) //存钱
 var balances = make(chan int) //查余额
 func Deposit(mony int) { deposits <- mony }
 func Balance() int { return <-balances}
 func teller(){
 var balance int //限制在一个goroutine内,不会读写冲突
 for {
 select {
 case mony := <-deposits:
 balance += mony
 case balances <- balance:
 }
 }
 }
 fun init(){
 go teller() //启动teller
 }
- 
避免数据读写冲突的方法: - 初始化后,变量只读
- 避免多个goroutine访问同一变量。比如30的例子,用管道通信的方式,避免了多goroutine的同时读写。
- 使用互斥锁。
 
- 
读写锁只有在竞争激烈时效果才比一般锁好,单个执行是不如一般锁,这是由于读写锁实现更复杂。 
- 
一些导致并行数据冲突的原因: - 读写冲突导致
- cpu缓存会导致并发的时候读到错误的缓存值,而锁操作可以把缓存刷入内存。多核cpu会分配并发线程到不同cpu,而每个cpu都有自己的缓存,会读到过期的数据
- 并行程序在相互之间没有变量逻辑关联的时候,编译器可能会交换执行顺序。
 
- 
延迟互斥初始化 sync.Once- 一种延迟初始化的示例:
 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20type memo struct{ 
 mem map[string]string
 mu sync.Mutex
 ready chan struct{} //mem 准备好后关闭
 }
 func (m *memo)Get(key string) string {
 m.mu.Lock()
 if m.mem == nil{
 //假设初始化的时候没有进行make
 //第一次get数据时,进行初始化。
 m.mem = make(map[string]string)
 m.mu.Unlock()
 close(m.ready)//广播数据准备完毕,并且只需要广播一次。
 }else{
 m.mu.Unlock()
 //由于第一次广播数据时,关闭了消息,后续的访问,都不会阻塞。
 <-m.ready
 }
 return m.mem[key]
 }
- 
可以使用竞态检测器 race deleteor来检测代码中是否有数据竞态的情况。使用方法在编译时增加-race参数
- 
goroutine和线程的差别 - goroutine有可增长的栈,而OS里的线程一般只有固定大小的2MB。goroutine栈的增长也是以2倍增长,通常初始值在2KB,最大一般可以到1GB
- 线程调度需要涉及到CPU上下文切换,而goroutine调度通常是由go调度器处理,而调度技术成为m:n调度(把m个goroutine调度到n个线程)
- goroutine没有标识,而OS线程通常都有线程标识。
 
- 
GOMAXPROCS值表示:使用多少个OS线程来执行goroutine。正在休眠的goroutine不占用线程,阻塞IO、其他系统调用执行中或非go语言写的函数这些goroutine需要一个独立的OS线程。
测试
- 
测试函数比如导入 testing包,除去开头函数,可选后缀必须大写开头。
- 
测试部分的代码,通常使用 _test.go来进行区分,而包通常在一个里。
- 
测试中 -run命令后面跟的是正则表达式,运行匹配的函数。匹配字符里,可以忽略Test头
- 
测试中的 t.Fatal函数必须heTest在同一个goroutine中。错误消息的一般格式"f(x)=y, want z"
- 
随机测试,构建符合模式的随机输入,来探测程序的边界。 
- 
测试程序和被测试程序,虽然都定义在一个包里,但是实际执行 go test和执行主程序时,都是分开的。
- 
测试程序中,不要调用 log.Fatal或os.Exit,这些调用会阻止跟踪的过程。通常认为这两个是main函数的特权调用。
- 
白盒测试可以通过修改一些全局量来达到测试部分功能的目的,比如代码中使用了全局函数指针 var sendMsg = fun(string) error{...},我们可以在白盒测试中修改改指针,来避免发送出错。
- 
外部测试包,通常用于外部调用来测试相应包,无法访问包内部变量(会单独存在一个包,package命名使用 <package>_test)。其他则和内部测试相同。
- 
有时候,外部测试包需要拥有对内部包的特殊访问权限,我们可以约定声明一个特殊的测试文件 export_test.go,而其存放的内容,仅仅是把包内部功能暴露。如fmt包中的export_test.go:1 
 2
 3package fmt 
 var IsSpace = isSpace
 ...
- 
如果我们要看我们自己编写的测试程序覆盖了多少源码,可以使用 go test -coverprofile=c.out ./来查看。使用go tool cover可以查看cover的帮助。c.out会输出程序中哪些代码被覆盖,哪些没覆盖。
- 
基准测试 Benchmark,执行命令go test -bench=<name>,同样<name>使用正则匹配。如果不指定-bench,则不执行基准测试。-benchmem可以看到内存的操作消耗。详细测试可参考
- 
我们可以通过 go test做代码的性能剖析,不同的报告类别告诉我们不同的性能情况:1 
 2
 3
 4
 5
 6
 7
 8go test -cpuprofile=cpu.out //CPU占用 
 go test -blockprofile=block.out //阻塞操作
 go test -memprofile=mem.out //内存占用
 ```
 一个显示CPU性能的示例:
 ```go
 go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http
 go tool pprof -text -nodecount=10 ./http.test cpu.log其中 http.test是执行go test时产生的中间文件(如果go test不带性能分析参数,那么就会删掉该中间文件)-nodecount=10限制输出10行
- 
Example函数也会被go test视为特殊函数。该函数必须通过语法编译,并且会关联Example同名后缀的函数,并在文档中以示例的方式展示。也可以执行Example函数,并且go test会自动将执行结果,和你函数最后的带有// 输出:后的注释进行比较,判断输出正确性。
环境及工具
- 工作空间GOPATH依赖的源码,现在通常都是通过go mod来进行管理。环境变量GOROOT,指定go的版本。go env能配置这些变量。
- 进行go build时,后面的相对路径前必须加./或../之类的,否则就会识别为参数。
- go build和- go install非常相似,区别在于- go install不会丢弃编译代码和命令。它们对于没有修改过的包都不会进行重新编译。
- 可以通过go doc来查看函数结构体等声明和注释,使用方法:go doc time,go doc time.Since等。当然我们自己也在包开发的时候也鼓励编写函数和包注释。
- godoc和- go doc功能一样,差别在于提供http的服务供网页查看。如- godoc -http :8000。- -analysis=type和- -analysis=pointer提供更丰富的文档。
- 内部包是以internal为父目录的目录下的包,只能提供给internal的父目录使用,其他地方无法使用。
- go list可以进行可用包的查询,- ...为通配符。- -json参数可以让包已json格式输出,- -f可以通过- text/template模板来定义输出格式。
- go的标准文件和_test.go文件在go test时会分成两个包进行编译。你可以通过go list -json <package>来查看包里的构成,GoFiles包含的内容在go build时用到,而TestGoFiles则只会在go test的时候用到。``XTestGoFiles`通常指外部测试列表
- 包的空导入import _ image/png,其作用是利用包中的init函数进行相关组件的注册。同理,我们也可以通过init函数来进行函数注册和模块注册。
约定及习惯
- 包名原则上和父目录的文件夹名称一致。如果不一致也能干活儿,但是你引用的import git.com/pkg/A的包,使用A路径下文件里函数的时候却是B.xxFunc,不觉得这么干有病么。
- 如果类的方法使用的是指针接收者,那么所有的方法都建议使用指针接收者,如果方法使用结构体接收者,那么所有有方法建议都用结构体接收者。
- 接口命名通常是动词+er,比如writer,controller等
- golang提倡平铺式结构,不提倡继承关系的结构。
- 程序结构,根据需求,以核心逻辑为主,细节由接口实现。
- 如果要提公共接口,公共接口往往专注一个能力,干一件事。不宜过长。
- 一个不错的接口设计经验:仅设计你所需要的接口
- golang命名建议:
- 变量、函数、结构体等建议驼峰命名
- 包名小写拼接,比如strconv,不建议str_conv
- 文件名看个人习惯,通常是小写拼接或者小写_拼接。基本除了驼峰类型,其他的都行。
 
- 好的测试程序:
- 测试程序也需要健壮性,和秩序跟进维护
- 测试程序要尽可能多的报告出错误信息,要尝试一次运行中报多个错误。
- 覆盖率
 
- 测试的约定函数名:
- Test开头,代表功能测试
- Benchmark开头,代表性能测试
- Example开头,代表示例函数
 
Cgo
- 
cgo用于在go中调用c的函数,当然如果你的c库很小,可以考虑移植它,如果性能对我们来说不是很关键,那么也可以用os/exec来执行c的可执行程序。cgo只用于复杂且对性能有所要求的C库。
 一个C写的程序:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13#include <bzlib.h> 
 int bz2compress(bz_stream *s, int action,
 char *in, unsigned *inlen, char *out, unsigned *outlen){
 s->next_in = in;
 s->avail_in = *inlen;
 s->next_out = out;
 s->avail_out = *outlen;
 int r = BZ2_bzCompress(s,action);
 *inlen -= s->avail_in;
 *outlen -= s->avail_out;
 return r
 }对应cgo部分的代码。 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
 46
 47
 48
 49
 50
 51
 52
 53
 54package main 
 /*
 #cgo CFLAGS: -I/usr/include
 #cgo LDFLAGS: -L/usr/lib -lbz2
 #include <bzlib.h>
 int bz2compress(bz_stream *s, int action,
 char *in, unsigned *inlen, char *out, unsigned *outlen);
 */
 //标识加入cgo,程序会分析注释中的相关内容
 import "C"
 import (
 "io"
 "unsafe"
 )
 type writer struct {
 w io.Writer
 stream *C.bz_stream
 outbuf [64 * 1024]byte
 }
 //NewWriter 对于bzip2压缩的流返回一个wirter
 func NewWriter(out io.Writer) io.WriteCloser {
 const (
 blockSize = 9
 verbosity = 0
 workFactor = 30
 )
 w := &writer{w: out, stream: C.bz2alloc()}
 C.BZ2_bzCompressInit(w.stream, blockSize, verbosity, workFactor)
 return w
 }
 func (w *writer) Write(data []byte) (int, error) {
 if w.stream == nil {
 panic("closed")
 }
 var total int
 for len(data) > 0 {
 inlen, outlen := C.uint(len(data)), C.uint(cap(w.outbuf))
 C.bz2compress(w.stream, C.BZ_RUN,
 (*C.char)(unsafe.Pointer)(&data[0]), &inlen,
 (*C.char)(unsafe.Pointer)(&w.outbuf), &outlen)
 total += int(inlen)
 data = data[inlen:]
 if _, err := w.w.Write(w.outbuf[:outlen]); err != nil {
 return total, err
 }
 }
 return total, nil
 }
结语
作为golang语言的半入门者,看完这本书,算是入门了。总的来说,书还是很不错的,看完过后,可以让你对golang有个整体的概念,更加熟悉语言本身的特性,以及学习到一些使用技巧。值得入门级别的go语言学习者仔细阅读,其中的一些疑惑可以自己编写代码执行来深入了解。
注:这个笔记本身没有什么阅读学习价值,读书笔记对于我来说是在我学习的时候更加帮助我记忆和理解,以及后续遇到某个问题时的快速查阅。