美文网首页
Go学习笔记笔记

Go学习笔记笔记

作者: 萧然AND沐橦 | 来源:发表于2018-04-03 14:09 被阅读18次

    类型

    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数组(不定长结构)保存了实际方法地址,从而实现在运行期对目标方法的动态调用。
      除此之外,接口还有一个重要特征:将对象复制给接口变量时,会复制该对象。

    相关文章

      网友评论

          本文标题:Go学习笔记笔记

          本文链接:https://www.haomeiwen.com/subject/witpgxtx.html