《go语言程序设计》读书笔记

传说中的go语言圣经,得看一看(•̀⌄•́)
                              —— By Jihan


主题:GO语言学习
类别:计算机->实用性+理论性论述书->golang
概要:从语法,特性,接口,工具,测试等方面系统性介绍go语言。总体来说是针对语言本身特性来进行介绍的。

前言

最近在设计一个项目的框架,所使用的就是golang语言,期望多看看书以及学习一些源码,从而有更优雅的架构设计。
书中有许多的练习程序,手动写代码,跑程序也是很重要的。

主要内容

太过于基础的语法,并不会在笔记中出现,这里主要记录一些go语言编写中需要注意的点。

基础语法

主要包含一些语法和关键语句特性相关内容。

  1. 如果传参使用map或者slice实际传递的是引用的拷贝,也就是指针的拷贝,那么函数里对map的操作对调用者来说也是可见的。比如下面的代码:

    1
    2
    3
    func insertMap(m map[int]string){
    m[1] = "1"
    }
  2. switch中的值会和每一个case进行比较,直到找到匹配的值。因此不要在case中放耗时的操作。select也是同样如此。

  3. 短变量声明一般用作局部变量中。示例:

    1
    2
    3
    in,err := os.Open(file)//声明两个变量in,err
    out,err := os.Open(out)//声明变量out,赋值变量err
    out,err := os.Open(out1)//报语法错误
  4. 多重赋值的时候,会先完全计算完右边的值,再赋值给左边。

    1
    x,y = x-y, x+y//可以完全得到预想的值,即x=x[旧]-y[旧], y=x[旧]+y[旧]
  5. 字符串string是不可变字节序列,不可越界,不可修改,运算只能产生新的变量,而非修改原有值。

  6. 无类型常量const往往拥有更高的精度和更广的取值范围,并且能够适用于很多地方。

  7. 数组的长度也是数组类型的一部分。

    1. 数组初始化:
    1
    2
    3
    q := [...]int{1,2}//同r类型,...会自己根据初始化数据个数填入
    r := [2]int{1,2}
    c := [...]int{99:-1}//len(c)=100, 除去c[99] = -1,其他都是0
  8. 数组直接传递,是值传递,不同其他语言是引用传递。如果想使用引用传递,需要定义成指针类型:func zero(a *[32]int)

  9. slice为空的判断,使用len(s) == 0 而不是s == nil

  10. go语言有可变长度的栈,最大可以达到1G,能更加安全的使用递归

  11. defer函数的实际执行顺序以调用顺序的倒叙执行。

    1. 当发生panic时,所有defer也将以倒叙执行。
  12. 内置函数recover只能在defer中调用,并且当发生panic时能终止当前的宕机,并且从发生panic的位置正常返回(不会往下执行)。

    1. 通常情况下,不建议在recover中进行程序恢复,这样更容易掩盖程序中的bug
    2. 常用recover的方式是提供一些更加有用的信息。

函数及变量作用域

  1. 作用域:

    1. 整个程序级别:int, new等内置类型或函数
    2. 包级别:同一个包的任何地方引用,比如包里的全局变量
    3. 文件级别:只有导入了该包才能使用的。比如fmt.Print
    4. 块儿级别:函数内部,循环内部等。
  2. 函数作为参数的时候,传递的作用域问题,本质上函数参数传递的是指针,那么使用的时候也是直接使用指针指向的函数,只要指针本身有效(不是野指针),函数都能正常执行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func 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
    }
  3. 同第三点所说,当结构体作为参数的时候,其他包是否有权限读取内部变量。类似json这种结构体中的内部变量就不会进行转换。实际上,直接读取肯定是无法读取的,写代码都编译不过。但可以通过反射获取,示例:

    1
    2
    3
    4
    func main(){
    vs := var_scope.VS{Out: 1}
    fmt.Println("Print var_scope VS", vs)
    }

    var_scope包:

    1
    2
    3
    4
    type 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
    6
      	if 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
    14
    func 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
    13
    type SendVar struct {
    Out int
    inner int
    }


    func main() {
    s := SendVar{
    1,
    2,
    }
    var_scope.PrintOtherStruct(s)
    }
  4. 匿名函数,func关键字后没有变量的函数,例如:strings.Map(func(r rune) rune{return r + 1}, "HAL-9000")。匿名函数有一些特性

    1. 匿名函数能够获取到整个词法环境,里层能够使用外层的变量。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func 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
    5
    visitAll := func(items []string){
    //...
    visitAll(m[item]) //compile error: undefind visitAll
    //...
    }
    1. 匿名函数在for循环中的外部变量引用,先看示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var 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则每次都会声明一个变量,隐藏在匿名函数中,而不会释放。

  5. 指针类型成员函数,使用非指针类型成员也能正常调用,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
    25
    package 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
    2
    A:{222}
    B:{{11}}

接口和反射

  1. nil也可以作为类方法的接收者,比如var a *NewStruct= (* NewStruct)(nil),但是通常不会这样使用。

  2. 接口就是提取一系列具体类型的共性而构成的。很多的常用系统函数都是提供接口值传递,支持自定义的类型传递(实现接口就行)。

  3. 接口值由两部分组成:类型和动态值。

    1. 如果动态值可以比较(不是map或者slice这类),那么接口值也是可以比较的。
    2. var w *io.writer = os.Stdout。接口值里的类型是*os.File,对应值是&os.File{fd=1}
    3. 注意含有空指针的非空接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func main(){
    var a *os.File
    check(a)
    }
    func check(w *io.writer){
    //注意,这个地方传入的w,其接口值不为空。因为接口值存在非空类型值`*os.File`
    if w == nil {
    panic("w is nil")
    }
    }
  4. error是一个接口,定义如下:

    1
    2
    3
    type error interface{
    Error() string
    }
  5. 断言:f, ok := x.(T)

  6. 有些函数包可以通过断言error的类型来判断错误类型。

  7. 可以通过断言来判断某个接口里是否包含某个方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func 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)
    }
    ....
    }
  8. 反射reflect

    1. 反射由两部分构成: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()来获取。

  9. 使用反射的注意事项(总结:没有必要用反射,就不要用反射):

    1. 反射代码是很脆弱的,很容易出bug,并且bug是在遇到对应类型的时候以panic的方式抛出。
    2. 反射无法做静态类型检测,特别是interface{}reflect.Value参数时,一定要说明参数访问和其他限制。并且大量使用反射也不便于理解。
    3. 使用反射是比特定类型优化的函数低一两个数量级。有些关键性的核心函数,还是要避免使用反射。
  10. unsafe包中包含了许多接近底层的一些操作,当然这些操作在C语言里面可能是常见的。如果不是很有必要,并不建议使用unsafe包,unsafe包会导致更不易察觉的错误,以及削弱代码可移植性。

    1. unsafe.Pointer指向一个T类型的指针,类似C里的void *unsafe.Pointer可以转换为uintptr,然后对指针进行数值计算。但是注意如果使用uintptr作为中间变量,由于其类型为一个数据,那么其对应的值所指向的值可能被垃圾回收移动或者回收,导致不易发现的错误。如:
    1
    pT := uintptr(unsafe.Poniter(new(T))) //语句执行后,创建的T就要被回收,pT值就变成野指针。

    如果要使用uintptr,需要谨慎使用,保证使用的最小范围。

  11. reflect.DeepEqual深度比较,会进行两个任意数据的类型,长度,和数据的比较。

  12. 接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以根据具体类型 P 直接辨识的:

    • 指针方法可以通过指针调用
    • 值方法可以通过值调用
    • 接口接收者是值的方法可以通过指针调用,因为指针会首先被解引用
    • 接口接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

goroutine和通道

  1. 协程之间是没有父子关系的,所有子协程都是一个等级,挂在main函数下。

    1. 父协程退出,子协程不会退出
    2. 函数退出,函数内的协程不会退出(main函数除外)
  2. 管道类型chan Tmap类似,传递都是进行指针传递。

    1
    2
    3
    4
    x, ok := <- ch
    if !ok {
    fmt.Fatal("channel closed.")
    }
    1. 管道可以手动关闭(也可以让gc自己回收),关闭后任何向管道里写入的操作都会崩溃,读取操作能够正常执行,读取完管道值后再读取的值就会零值。
    2. 单向通道:类型chan<- T只能发送的的通道,类型<-chan T只能接收的通道。类型chan T传值给单向通道的时候,都会进行隐式转换到相应的结构。
    3. 带有缓冲的通道,通道的数据存取类似于管道,先进先出。如果粗暴的在一个goroutine中将管道作为队列使用,可能会出现永久阻塞的风险。
    4. select中,如果通道是nil,则改分支永远不会被选中,该特性可以用来启用或禁用某些特性。
    5. 可以在goroutine中使用管道作为令牌桶,控制goroutine的并发量:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var 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
    }()
    }
  3. 不要通过共享内存来通信,应该使用通信来共享内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package 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
    }
  4. 避免数据读写冲突的方法:

    1. 初始化后,变量只读
    2. 避免多个goroutine访问同一变量。比如30的例子,用管道通信的方式,避免了多goroutine的同时读写。
    3. 使用互斥锁。
  5. 读写锁只有在竞争激烈时效果才比一般锁好,单个执行是不如一般锁,这是由于读写锁实现更复杂。

  6. 一些导致并行数据冲突的原因:

    1. 读写冲突导致
    2. cpu缓存会导致并发的时候读到错误的缓存值,而锁操作可以把缓存刷入内存。多核cpu会分配并发线程到不同cpu,而每个cpu都有自己的缓存,会读到过期的数据
    3. 并行程序在相互之间没有变量逻辑关联的时候,编译器可能会交换执行顺序。
  7. 延迟互斥初始化sync.Once

    1. 一种延迟初始化的示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    type 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]
    }
  8. 可以使用竞态检测器race deleteor来检测代码中是否有数据竞态的情况。使用方法在编译时增加-race参数

  9. goroutine和线程的差别

    1. goroutine有可增长的栈,而OS里的线程一般只有固定大小的2MB。goroutine栈的增长也是以2倍增长,通常初始值在2KB,最大一般可以到1GB
    2. 线程调度需要涉及到CPU上下文切换,而goroutine调度通常是由go调度器处理,而调度技术成为m:n调度(把m个goroutine调度到n个线程)
    3. goroutine没有标识,而OS线程通常都有线程标识。
  10. GOMAXPROCS值表示:使用多少个OS线程来执行goroutine。正在休眠的goroutine不占用线程,阻塞IO、其他系统调用执行中或非go语言写的函数这些goroutine需要一个独立的OS线程。

测试

  1. 测试函数比如导入testing包,除去开头函数,可选后缀必须大写开头。

  2. 测试部分的代码,通常使用_test.go来进行区分,而包通常在一个里。

  3. 测试中-run命令后面跟的是正则表达式,运行匹配的函数。匹配字符里,可以忽略Test

  4. 测试中的t.Fatal函数必须heTest在同一个goroutine中。错误消息的一般格式"f(x)=y, want z"

  5. 随机测试,构建符合模式的随机输入,来探测程序的边界。

  6. 测试程序和被测试程序,虽然都定义在一个包里,但是实际执行go test和执行主程序时,都是分开的。

  7. 测试程序中,不要调用log.Fatalos.Exit,这些调用会阻止跟踪的过程。通常认为这两个是main函数的特权调用。

  8. 白盒测试可以通过修改一些全局量来达到测试部分功能的目的,比如代码中使用了全局函数指针var sendMsg = fun(string) error{...},我们可以在白盒测试中修改改指针,来避免发送出错。

  9. 外部测试包,通常用于外部调用来测试相应包,无法访问包内部变量(会单独存在一个包,package命名使用<package>_test)。其他则和内部测试相同。

  10. 有时候,外部测试包需要拥有对内部包的特殊访问权限,我们可以约定声明一个特殊的测试文件export_test.go,而其存放的内容,仅仅是把包内部功能暴露。如fmt包中的export_test.go:

    1
    2
    3
    package fmt
    var IsSpace = isSpace
    ...
  11. 如果我们要看我们自己编写的测试程序覆盖了多少源码,可以使用go test -coverprofile=c.out ./来查看。使用go tool cover可以查看cover的帮助。c.out会输出程序中哪些代码被覆盖,哪些没覆盖。

  12. 基准测试Benchmark,执行命令go test -bench=<name>,同样<name>使用正则匹配。如果不指定-bench,则不执行基准测试。-benchmem可以看到内存的操作消耗。详细测试可参考

  13. 我们可以通过go test做代码的性能剖析,不同的报告类别告诉我们不同的性能情况:

    1
    2
    3
    4
    5
    6
    7
    8
    go 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行

  14. Example函数也会被go test视为特殊函数。该函数必须通过语法编译,并且会关联Example同名后缀的函数,并在文档中以示例的方式展示。也可以执行Example函数,并且go test会自动将执行结果,和你函数最后的带有// 输出:后的注释进行比较,判断输出正确性。

环境及工具

  1. 工作空间GOPATH依赖的源码,现在通常都是通过go mod来进行管理。环境变量GOROOT,指定go的版本。go env能配置这些变量。
  2. 进行go build时,后面的相对路径前必须加./../之类的,否则就会识别为参数。
  3. go buildgo install非常相似,区别在于go install不会丢弃编译代码和命令。它们对于没有修改过的包都不会进行重新编译。
  4. 可以通过go doc来查看函数结构体等声明和注释,使用方法:go doc time, go doc time.Since等。当然我们自己也在包开发的时候也鼓励编写函数和包注释。
  5. godocgo doc功能一样,差别在于提供http的服务供网页查看。如godoc -http :8000-analysis=type-analysis=pointer提供更丰富的文档。
  6. 内部包是以internal为父目录的目录下的包,只能提供给internal的父目录使用,其他地方无法使用。
  7. go list可以进行可用包的查询,...为通配符。-json参数可以让包已json格式输出,-f可以通过text/template模板来定义输出格式。
  8. go的标准文件和_test.go文件在go test时会分成两个包进行编译。你可以通过go list -json <package>来查看包里的构成,GoFiles包含的内容在go build时用到,而TestGoFiles则只会在go test的时候用到。``XTestGoFiles`通常指外部测试列表
  9. 包的空导入import _ image/png,其作用是利用包中的init函数进行相关组件的注册。同理,我们也可以通过init函数来进行函数注册和模块注册。

约定及习惯

  1. 包名原则上和父目录的文件夹名称一致。如果不一致也能干活儿,但是你引用的import git.com/pkg/A的包,使用A路径下文件里函数的时候却是B.xxFunc,不觉得这么干有病么。
  2. 如果类的方法使用的是指针接收者,那么所有的方法都建议使用指针接收者,如果方法使用结构体接收者,那么所有有方法建议都用结构体接收者。
  3. 接口命名通常是动词+er,比如writer, controller
  4. golang提倡平铺式结构,不提倡继承关系的结构。
  5. 程序结构,根据需求,以核心逻辑为主,细节由接口实现。
  6. 如果要提公共接口,公共接口往往专注一个能力,干一件事。不宜过长。
  7. 一个不错的接口设计经验:仅设计你所需要的接口
  8. golang命名建议:
    1. 变量、函数、结构体等建议驼峰命名
    2. 包名小写拼接,比如strconv,不建议str_conv
    3. 文件名看个人习惯,通常是小写拼接或者小写_拼接。基本除了驼峰类型,其他的都行。
  9. 好的测试程序:
    1. 测试程序也需要健壮性,和秩序跟进维护
    2. 测试程序要尽可能多的报告出错误信息,要尝试一次运行中报多个错误。
    3. 覆盖率
  10. 测试的约定函数名:
    1. Test开头,代表功能测试
    2. Benchmark开头,代表性能测试
    3. Example开头,代表示例函数

Cgo

  1. 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
    54
    package 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语言学习者仔细阅读,其中的一些疑惑可以自己编写代码执行来深入了解。
注:这个笔记本身没有什么阅读学习价值,读书笔记对于我来说是在我学习的时候更加帮助我记忆和理解,以及后续遇到某个问题时的快速查阅。

-------------本文结束感谢您的阅读-------------