Go学习笔记笔记

类型

  1. 引用类型特指slice、map、channel这三种预定义类型。
  2. 内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关的初始化。(除new/make外,还可以使用初始化表达式,编译器生成的指令基本相同)
  3. 具有相同声明的未命名类型被视作同一类型:
    a). 具有相同基类型的指针。
    b). 具有相同元素类型和长度的数组(array)。
    c). 具有相同元素类型的切片(slice)。
    d). 具有相同键值类型的字典(map)。
    e). 具有相同数据类型及操作方向的通道(channel)。
    f). 具有相同字段序列(字段名、字段类型、标签,以及字段顺序)的结构体(struct)。
    g). 具有相同签名(参数和返回值列表,不包括参数名)的函数(func)。
    h). 具有相同方法集(方法名、方法签名,不包括顺序)的接口(interface)。
  4. 未命名类型转换规则:
    a). 所属类型相同。
    b). 基础类型相同,且其中一个是未命名类型。
    c). 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型。
    d). 将默认值nil赋值给切片、字典、通道、指针、函数或接口。
    e). 对象实现了目标接口。

表达式

  • 指针类型支持相等运算符,但不能做加减法运算和类型和转换。可以通过unsafe.Pointer将指针转化为uintptr后进行加减法运算,但可能会造成非法访问。
  • Pointer类似C语言中的void*万能指针,可用来转换指针类型。他能安全持有对象或对象成员,但uintptr不行。后者仅仅是一种特殊的整形,并不引用对象,无法阻止垃圾回收器回收对象内存。
  • for ... range 会赋值底层对象,如数组,则会复制底层数组。可以改用切片作为range的对象,减少复制整个数组的开销。相关的数据类型中,字符串、切片本身基本结构是个很小的结构体,而字典、通道本身是指针的封装,复制成本都很小,无须专门的优化。
  • 如果range的对象是一个函数,那么该函数也只被调用一次。
  for i := range data() {

  }
  • 切片用来代替数组传参可避免复制开销。并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而小数组在栈上的拷贝消耗也未必就比make代价大。
  • 新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见。
  • 从表面上看,指针参数的性能要更好一些,但实际上得具体分析。被复制的指针会延长目标对象生命周期,可能还会导致它分配到堆上,那么其性能消耗就得加上堆内存分配和垃圾回收的成本。

函数

  1. Go中函数特点:
    a). 无须前置声明
    b). 不支持命名嵌套定义(nested)
    c). 不支持同名函数重载(overload)
    d). 不支持默认参数
    e). 支持不定长参数
    f). 支持多返回值
    g). 支持命名返回值
    h). 支持匿名函数和闭包
  2. 函数只能判断其是否为nil,不支持其它操作
  3. 变参本质上就是一个切片。只能接收一到多个类型参数,且必须放在列表尾部。
  4. 将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别,当然,编译器会为匿名函数生成一个“随机”符号名。
  5. 闭包是函数和引用环境的组合体。本质上返回的是一个funcval结构。
138 type funcval struct {
139     fn uintptr
140     // variable-size, fn-specific data here
141 }
  1. 正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,设置被分配到堆内存。还有延迟求值的特性。
func test() []func () {
  var s []func()
  for i:=0; i < 2 ; i++ {
    s = append ( s , func() {
      println(&i , i ) 
    })
  }
  return s
}

func main() {
  // 这里的test只会被调用一次
  for _, f := range test() {
    f()
  }
}
// 输出结果
0xc420070000 2
0xc420070000 2
// 解决办法
for i:=0 ; i < 2 ; i++ {
  x := i
  s = append (s , func() {
    println(&x , x )
  })
}
  1. return 语句不是ret汇编指令,它会先更新返回值。return和panic语句都会终止当前函数流程,引发延迟调用。
  2. 千万记住,延迟调用在函数结束时才被执行。不合理的使用方式会浪费资源,甚至造成逻辑错误。如对一个日志文件的close使用defer可能导致文件不能及时关闭,资源不能释放。延迟调用的性能和直接手工调用效率相差4倍~5倍。Go 1.5 version
  3. 实现接口的方法集的receiver必须不是pointer reciver,赋值给接口的实例必须不是一个pointer实例。
  4. 在延迟调用中再次panic,不会影响后续延迟调用执行。而recover之后panic,可能被再次捕获,另外,recover必须在延迟调用函数中执行才能正常工作。
  5. 在正式代码中,我们不能忽略error返回值,应严格检查,否则可能会导致错误的逻辑状态。调用多返回值函数时,除error外,其它返回值同样需要关注,如os.File.Read方法,它同时会返回剩余内容和EOF
  6. 大量的error处理的解决思路:
    • 使用专门的检查函数处理错误逻辑,简化检查代码
    • 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)
    • 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
  7. 除非是不可恢复性、导致系统无法工作的错误,否则不建议使用panic

数据

  1. 动态构建字符串容易造成性能问题,通常推荐使用strings.Join函数,它会统计所有参数长度,并一次性完成分配操作。
    字符串buffer可以用类似于vector.reserve(),也能完成相似的工作,并且性能相当
  var b bytes.Buffer
  b.Grow(1000)
  b.WriteString("hello world")

对于数量较小的字符串格式化拼接,可以使用fmt.Sprintf、text/template
字符串操作通常在堆上分配内存,这会对Web等高并发应用会造成较大影响,会有大量字符串要做垃圾回收。建议使用[]byte缓存池,或在栈上自行拼装等方式来实现zero-garbage。

  1. 内置函数len和cap都返回第一纬度的长度
  2. 数组传参数时候,为了减少内存拷贝,可以用指针接收或者切片
func main() {
    var a []int
    b := []int {}
    println(a == nil , b == nil)
}

上述两种方式定义的区别在与,a仅仅定义了一个[]int类型的变量,并未执行初始化操作,而b则用初始化表达式完成了全部创建过程。
自然的,a为nil,b不为nil。
另外,a==nil仅仅表示a是一个未初始化的切片对象,切片本身依然会分配所需内存。可以直接对切片做slice[:]操作,同样返回nil

  1. 并非所有时候都适合用切片代替数组,因为切片地城数组可能会在栈上分配内存。而且小数组在栈上拷贝的消耗也未必就比make代价大。
  2. slice在append时候,如果超出当前slice的cap限制,则会重新分配内存
    新分配的数组长度是原cap的2倍,并非原数组的2倍(并非总是2倍,对于较大的切片,会尝试扩容1/4,以节约内存)
  3. 向nil切片追加数据时,会为其分配底层数组内存
  4. 正因为可能会重新分配内存,所以需要留足空间,防止重新分配内存的情况
  5. 如果切片长时间引用大数组中很小的片段,那么建议独立建立切片,复制出所需要数据,以便原数组内存可以被GC随时回收。
  6. 字典不能被cap,并被设置为no addressable,所以当需要更新map的key-value时候,应当先读取值存变量中,修改value之后,在重新赋值:
    type user struct {
        name string
        age byte
    }

    func main() {
        m := map[int]user {
            1: {"Tom",19},
        }
        u := m[1]
        u.age += 1 
        m[1] = u
    }

但如果内部存储的是指针类型,则可以直接修改:

    m2 := map[int]*user {
        1 : &user { "wind" , 20 },
    }
    m2[1].age++
  1. 不能对nil字典做写操作,但可以读。
  2. 内容为空的字典,与nil是不同的:
    var m1 map[string]int       // nil 字典
    m2 := map[string]int{}     // 内容为空的字典
  
    println( m1 == nil , n2 == nil )
    // true false
  1. 字典和切片对象本身就是指针封装,传参数时,无需要再去地址
    最好预先分配好足够的空间,减小map扩张时候,内存分配和重新hash造成的运行时开销。
  2. 只有在所有的结构字段都支持相等操作时候,才能对结构进行相等比较。
  3. 空结构(struct{})没有字段结构类型,无论是单个struct{}变量,或者struct{}数组,长度都为0。尽管没分配数组内存,但依然可以操作元素,对应切片的len和cap属性也正常。这类“长度”为0的对象通常都指向runtime.zerobase的变量。
  4. 空结构可作为通道元素类型,用于事件通知。
  5. 未命名类型没有名字标识,无法作为匿名字段,接口指针和多级指针都不能作为匿名字段。
  6. 不能将基础类型和其指针类型同时嵌入,因为两者隐式名字相同。
  7. tag并不是注释,而是对字段进行描述元数据。尽管其不属于数据成员,但确实类型的组成部分(在运行时,可以用反射获取标签信息。被作为格式校验,数据库关系映射等)

方法

  1. 不能用多级指针调用方法,指针类型的receiver必须是合法指针(包括nil都可以),或者能获取实例地址
    type X struct{}
    func (x *X) test() {
        println("hi!",x) 
    }
    func main() {
        var a *X
        a.text()             // 相当于 test(nil)
    }
    X{}.test()             // 错误 cannot take the address of X literal
  1. 如何选择方法的reveiver类型?
    • 要修改实例状态,用*T
    • 无须修改状态的小对象或固定值,建议用T
    • 大对象建议用*T,以减少复制成本
    • 引用类型、字符串、函数等指针包装对象,直接用T
    • 若包含Mutex等同步字段,用*T,避免因为复制造成锁操作无效
    • 其它无法确定的情况,都用*T
  2. 方法会有同名遮蔽问题,利用这种特性,可以实现类似覆盖(override)操作。(name hiding)
  3. 类型集的判别:
    • 类型T方法集包含所有receiver T方法
    • 类型*T方法集包含所有receiver T + *T方法
    • 匿名嵌入S,T方法集包含所有receiver S方法
    • 匿名嵌入S,T方法集包含所有receiver S+S方法
    • 匿名嵌入S或S,T方法集包含所有receiver S+*S方法
  4. 方法集仅影响接口实现和方法表达式转换,与通过实例或者实例指针调用方法无关。实例并不使用方法集,而是直接调用(通过隐士字段名)
  5. 面向对象的三大特征“封装”、“继承”和“多态”,Go仅实现了部分特征,它更倾向于“组合优于继承”这种思想。将模块分解成相互独立的更小但愿,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。
  6. Method Expression 和 Method Value的区别:
  7. 通过类型引用的method expression 会被还原为普通函数样式,receiver是第一参数,调用时须显式传递。类型可以是T或者*T,只要目标方法存在于该类型方法集中即可
    type N int
    func (n N) test() {
        fmt.Printf("test.n:%p,%d\n" , &n , n )
    }
    func main() {
        var n N = 25
        fmt.Printf("main.n: %p,%d\n" , &n , n)

        f1 := N.test                   // func (n N)
        f1(n)                              // 
        f2 := (*N).test            // func(n *N)

        f2(&n)                        // 按方法集中的签名传递正确类型的参数
    }
  1. method value,参数签名不会改变,依旧按照正常方式调用。但当method value 被赋值给变量或作为参数传递时,会立即计算并复制该方法执行锁需要的receiver对象,与其绑定,以便在稍后执行时,能隐式传入receiver参数。
    type N int 
    
    func (n N) test() {
        fmt.Printf("test.n: %p, %v\n" , &n ,n)
    }

    func main() {
        var n N = 100
        p := &n
        
        n++
        f1 := n.test             // 因为test方法的receiver是N类型
                                        // 因此复制n , 等于101
        n++
        f2 := p.test             // 复制p指向的值 等于102

        n++
        fmt.Prinf("main.n: %p,%v\n" , p , n )

        f1()
        f2()
    }

    type N int 
    
    func (n N) test() {
        fmt.Printf("test.n: %p, %v\n" , &n ,n)
    }

    func main() {
        var n N = 100
        p := &n
        
        n++
        f1 := n.test             // 因为test方法的receiver是N类型
                                        // 因此复制n , 等于101
        n++
        f2 := p.test             // 复制p指向的值 等于102

        n++
        fmt.Prinf("main.n: %p,%v\n" , p , n )

        f1()
        f2()
    }
// main.n: 0xc42007c008,103                                   
// test.n: 0xc42007c020,101                        
// test.n: 0xc42007c030,102          
  1. 编译器会为method value生成一个包装函数,实现间接调用。至于receiver复制,和闭包的实现方法基本相同,打包成funcval,经由DX寄存器传递。
  2. 当method value作为参数时,会复制含receiver在内的整个method value,当目标方法的receiver是指针类型,那么被复制的仅是指针。

接口

  1. 接口除了类型以来,有助于减少用户可视方法,屏蔽内部结构和实现细节。但接口实现机制会有运行期开销。对于相同包,或者不会频繁变化的内部模块之间,并不需要抽象出接口来强行分离。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。
  2. 从内部实现来看,接口自身也是一种结构类型,只是编译器会对其作出很多限制。
    type iface struct {
        tab *itab
        data unsafe.Pointer
    }
  • 接口不能有字段
  • 不能定义自己的方法
  • 只能声明方法,不能实现
  • 可嵌入其它接口类型
  1. 编译器根据方法集判断是否实现了接口。接口变量的默认值是nil,如果实现接口的类型支持,可以做相等运算。
  2. 嵌入其它接口类型,相当于将其声明的方法集导入。这就要求不能有同名方法,因为不支持重载。还有,不能嵌入自身或者循环嵌入,那会导致递归错误。
  3. 超级接口变量可以隐士转换为子集,反过来不行。
  4. 接口使用一个名为itab的结构存储运行期所需的相关类型信息。
   type iface struct {
       tab  *itab       // 类型信息
       data unsafe.Pointer     // 实际对象指针
   }
   type itab struct {
       inter   *interfacetype      // 接口类型
       _type   *_type                 // 实际对象类型
       fun     [1]uintptr             // 实际对象方法地址
   }
  1. 相关类型信息里保存了接口和实际对象的元数据。同时,itab还用fun数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。
    除此之外,接口还有一个重要特征:将对象复制给接口变量时,会复制该对象。

推荐阅读更多精彩内容

  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 70,831评论 26 501
  • 1.安装 https://studygolang.com/dl 2.使用vscode编辑器安装go插件 3.go语...
    go含羞草阅读 412评论 0 6
  • 前言 把《C++ Primer》读薄系列笔记全集。 目录 第I部分:C++基础 开始学习C++ 变量和基本类型 字...
    尤汐_Jennica阅读 3,153评论 1 35
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    萌萌的小伟哥阅读 445评论 0 0
  • 今天加人几个,昨晚的课是刚刚听的,学到不少关于问的知识,或者怎样去提问,马上上班了,抽时间写总结。今天回访客户10...
    罗敏儿阅读 27评论 0 0