Go 语言实战 -- 读书笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shida_csdn/article/details/86222202

第一章,关于 go 语言的介绍

1.  go runtime 提供线程池处理 goroutine,goroutine != 线程,而是类似于 task;

2.  channel 只能保证 goroutine 间数据传输同步,如果传递的是指针,针对指针内容的修改可能需要额外同步机制;

3.  go 语言崇尚:组合优于继承,一个类型由其他微小类型组合而成

4.  go 语言不需要去声明某个实例实现某个接口,只需要实现这组行为就好。

     这又叫鸭子类型  ----- 如果它叫起来像鸭子,那它就可能是只鸭子。

第二章,快速开始一个 go 程序

1.  如果 main 函数不在 main 包里,构建工具就不会生成可执行的文件;

2.  所有处于同一个文件夹里的代码文件,必须使用同一个包名。

     注意:

          import 只是告诉编译器去哪个文件夹里寻找,真正调用还得看文件头 package 定义的名字是什么

          但按照惯例,包和文件夹同名,import 时也可对包名(原来的 package 定义名字)起别名;

3.  程序中每个代码文件里的 init 函数都会在 main 函数执行前调用;

4.  与第三方包不同,从标准库中导入代码时,只需要给出要导入的包名;

5.  在 Go 语言里,标识符要么从包里公开,要么不从包里公开,

     其实,其他包可以间接访问不公开的标识符

     例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,

     哪怕调用者不是在这个包里声明的,都可以访问这个值;

6.  在 Go 语言中,所有变量都被初始化为其零值;

7.  指针变量可以方便地在函数之间共享数据;

8.  在 Go 语言中,所有的变量都以值的方式传递;

      因为指针变量的值是所指向的内存地址,在函数间传递指针变量,是在传递这个地址值,

      所以依旧被看作以值的方式在传递;

9.  有了闭包,函数可以直接访问到那些没有作为参数传入的变量;

     匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量本身;

     使用闭包要注意,内部会持续感知(同步)到外部的变化,这与传参有一定差异;

      

10.  interface{} 的值作为参数,一般会配合 reflect 包里提供的反射功能一起使用;

11.  空结构在创建实例时,不会分配任何内存;

12.  new 返回指针 new(T);

13.  因为大部分方法在被调用后都需要维护接收者的值的状态,

        所以,一个最佳实践是,将方法的接收者声明为指针

        有状态一定使用指针,无状态可以使用值

14.  与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大不同:

        使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时候被调用

        

        使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用;

第三章,打包和工具链

1.  同一个目录下的所有 .go 文件必须声明同一个包名(必须,否则报错,编译失败);

2.  把两个或多个目录下的 .go 文件都声明为同一个包名,只引入其中一个目录能编译,但是丧失了目录包内可见性

     不同目录不具备包可见性,编译器其实是当成不同包处理的,只是包名恰好相同而已

     这就是不同目录的同名包

     如果同时从不台目录引入相同名字的包,而且不重命名包名,将编译失败;

3.  GOPATH 可设置多个目录,按顺序查找;

     编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录

4.  每个包可以包含任意多个 init 函数;

5.  直接执行 go build ,其实是编译当前目录的包;

6.  3 个点表示匹配所有的字符串,

     go build <somepath>/... 表示编译 <somepath> 下所有包;

7.  打开本地文档服务器(会整理本机所有包的文档,包括自定义的), godoc -http=:6060;

8.  推荐使用的包管理工具是 godep,用法说明查看 godep 安装使用介绍;     

9.  可以为每个工程设置不同的 GOPATH,以保持源代码和依赖的隔离;

第四章,数组、切片和映射

1.  go 语言提供的三种存储集合数据的数据结构:数组、切片、映射;

2.  数组是切片和映射的基础数据结构;

3.  数组:其占用的内存是连续分配的;

4.  在 Go 语言里,数组是一个值。这意味着数组可以用在赋值操作中。

     变量名代表整个数组,因此,同样类型的数组可以赋值给另一个数组;

     数组变量的类型包括数组长度和每个元素的类型

     只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值;

5.  根据内存和性能来看,在函数间传递数组是一个开销很大的操作

     在函数之间传递变量时,总是以值的方式传递的。

     如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。

     因此编程时可以考虑传递数组的指针。

6.  切片的动态增长是通过内置函数 append 来实现的;

7.  切片使用 make 初始化,如果只指定长度,那么切片的容量和长度相等;

     分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,

     但是初始化后并不能访问所有的数组元素

8.  基于切片创建新切片,底层数组是共享的;

9.  nil 切片:var slice []int ;空切片:slice := make([]int, 0);

     nil 切片可以用来表示异常返回的结果(判断是否为 nil),空切片则可以表示查询结果为空;

     零切片 其实并不是什么特殊的切片,它只是表示底层数组的二进制内容都是零;

     空切片的指针地址是一个特殊地址,所有空切片共享这个内存地址;

     空切片和 nil 切片都没有分配存储空间;

     

     

10.  切片的容量,一般就是从数组指针位置到底层数组末尾的大小,

       但也可以单独限制切片容量(使其小于数组容量);

       

11.  切片越界访问(超过其长度范围)会导致运行时异常,即便还有剩余容量;

       必须使用 append 函数使用了这些剩余容量,才能用下标访问;

12.  append 操作有可能只是对底层数组元素做了修改,可能会影响到共享该数组的其它切片;

       

13.  在切片的容量小于 1000 个元素时,总是会成倍地增加容量。

       一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。

14.  限制切片容量,使用三索引方式:[起始位置:长度终止位置:容量终止位置]

       如果试图设置的容量比可用的容量还大,就会得到一个语言运行时错误

       

       

15.  如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,

       与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改

       这也是使用三索引的原因之一

16.  for range 迭代切片时,创建了每个元素的副本,而不是直接返回对该元素的引用

  for index, value := range slice {
      fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",value, &value, &slice[index])
  }

  //  这种 for 循环,value 的地址是不变的,而仅仅是不停地赋新值

17.  在函数间传递切片就是要在函数间以值的方式传递切片。

       由于切片的尺寸很小,在函数间复制和传递切片成本也很低。

18.  map 迭代时不保证键有序,

       因为插入时采用的散列法,不保证按照相同顺序插入数据生成的 map 结构也一样

19.  映射的键可以是任何值。

       这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==运算符做比较。

       切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键(编译错误)

20.  nil 映射:var colors map[string]string,空映射:colors := map[string]string{}
       nil 映射不能保存键值对(runtime error)

21.  使用 delete 从 map 删除键值对:delete(colors, "Coral")

22.  在函数间传递映射并不会制造出该映射的一个副本,属于引用传递,

       实际上,当传递映射给一个函数,并对这个映射做了修改时,

       所有对这个映射的引用都会察觉到这个修改

23.  map 可以保存 <nil,nil> 键值对,map 没有容量限制,不适用于 cap 函数

第五章,go 语言的类型系统

1.  任何时候,创建一个变量并初始化为其零值,习惯是使用关键字 var

     对于结构体类型,结构体内每个值都会初始化为其零值,该结构体变量不可能为 nil

     而接口类型的变量,可以赋值为 nil

2.  两种不同类型的值即便互相兼容,也不能互相赋值。编译器不会对不同类型的值做隐式转换

3.  值接收者的方法,通过值变量调用方法时,其实是把值变量的副本传给了方法

     如果使用对应的指针调用,则先取出指针的值,然后创建一个副本传给方法

4.  数值类型、字符串类型和布尔类型,这些类型本质上是原始的类型。

     因此,当对这些值进行增加或者删除的时候,会创建一个新值(值传递)。

     基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本

5.  切片、映射、通道、接口和函数类型,这类引用类型,引用类型的变量本身就是一个指针

     因为指针本来就适合通过值传递的,

     因此当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本

6.  判断是使用值类型还是指针类型作为接收者,要看其本质类型是不是原始类型

     即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递

7.  是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。

     这个决策应该基于该类型的本质。

8.  接口赋值前后的变化

     

     

     接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。

     这个内部表叫作 iTable,包含了所存储的值的类型信息。

     iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。

     第二个字是一个指向所存储值的指针。

     将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。

9.  因为不是总能获取一个值的地址,所以值的方法集只包括了使用值接收者实现的方法

     
10.  嵌入类型,

       属性提升使得嵌入类型的值能被直接访问,方法提升使得嵌入类型的方法能被直接调用

第六章,并发

1.  CSP(Communicating Sequential Processes)通信顺序进程 

     是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,

     而不是对数据进行加锁来实现同步访问。

2.  goroutine 的执行方式

     

     注意:M2 线程执行完 G4 任务后,会保存好,以便之后可以继续使用

     如果一个 goroutine 需要做一个网络 I/O 调用,流程上会有些不一样。

     在这种情况下,goroutine 会和逻辑处理器分离,并移到集成了网络轮询器的运行时。

     一旦该轮询器指示某个网络读或者写操作已经就绪,对应的 goroutine 就会重新分配到逻辑处理器上来完成操作。

3.  runtime.GOMAXPROCS(1) 设置逻辑处理器数量

4.  sync.WaitGroup 计数信号量的基本用法

var wg sync.WaitGroup
wg.Add(2)

go func(){
    defer wg.Done()
    ...
}

go func(){
    defer wg.Done()
    ...
}

wg.Wait()

5.  go build -race // 用竞争检测器标志来编译程序

6.  sync/atomic 原子函数方式修改共享变量:atomic.AddInt64(&counter, 1)

     外两个有用的原子函数是 LoadInt64 和 StoreInt64

     这两个函数提供了一种安全地读和写一个整型值的方式。

7.  sync.Mutex 互斥锁方式保护共享变量

var mutex sync.Mutex

mutex.Lock()

{
    // 操作共享变量
}

mutex.Unlock()

8.  无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证

9.  当通道关闭后, goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。

第七章,并发模式

1.  runner/pool/work

第八章,标准库

1.  iota 

const (
Ldate = 1 << iota     // 1 << 0 = 000000001 = 1
Ltime                       // 1 << 1 = 000000010 = 2
Lmicroseconds       // 1 << 2 = 000000100 = 4
Llongfile                 // 1 << 3 = 000001000 = 8
Lshortfile                // 1 << 4 = 000010000 = 16
...
)

2.   log

// 这个示例程序展示如何使用最基本的 log 包
package main

import (
"log"
)

func init() {
   log.SetPrefix("TRACE: ")
   log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}

func main() {
   // Println 写到标准日志记录器
   log.Println("message")

   // Fatalln 在调用 Println()之后会接着调用 os.Exit(1)
   log.Fatalln("fatal message")

   // Panicln 在调用 Println()之后会接着调用 panic()
   log.Panicln("panic message")
}

//  正式项目推荐使用 glog

3.   json 编码/解码

      自动生成 type 定义,推荐 gojson

      解码 json.可以选择 json.NewDecoder 以及 json.Unmarshal

      编码 json 可以选择  json.NewEncoder 以及 json.Marshal

4.  io.Writer 和 io.Reader 的 Write 、Read 方法是针对调用者而言的,

     xx.Write 指的是向 xx 写入(xx 被写入), xx.Read 指的是从 xx 读取(xx被读取)

第九章,测试和性能

1.  Go 语言的测试工具只会认为以_test.go 结尾的文件是测试文件。

2.  func TestDownload(t *testing.T) {}

     t.Log、t.Fatal、t.Error ,当执行到 Fatal 或 Error 时,表明测试失败

     Fatal 中止当前测试,Error 不中止

3.  基准测试

func BenchmarkSprintf(b *testing.B) {
   number := 10

   b.ResetTimer()

   for i := 0; i < b.N; i++ {
         fmt.Sprintf("%d", number)
   }
}

      go test -v -run="none" -bench="BenchmarkSprintf"

========

其他

1.  defer、return、返回值三者的执行逻辑应该是:

     return 最先执行,return 负责将结果写入返回值中;接着 defer 开始执行一些收尾工作;最后函数携带当前返回值退出。

猜你喜欢

转载自blog.csdn.net/shida_csdn/article/details/86222202