Go语言特性

  1. Go使用了更加智能的编译器,简化了解决依赖的算法,编译器只会关注那些直接被引用的库,而不是像Java、C和C++一样要遍历依赖链中所有依赖的库,编译速度更快
  2. 作为一种静态类型语言,Go提供了类型安全的特性,更容易检查类型错误等bug
  3. Go语言中用于并发的goroutine占用的内存远少于线程,并且使用它所需要的代码更少
  4. 通道(channel)可以让goroutine之间进行安全的数据通信,可以帮用户避免其他语言里常见的共享内存访问的问题
  5. 其他语言如果使用全局变量或共享内存,必须使用复杂的锁规则来防止对同一个变量的不同步修改。通道可以保证同一时刻只会有一个goroutine修改数据
  6. Go提供了灵活的、无继承的类型系统,依然支持面向对象开发。在Go中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型
  7. Go还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模,用户不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口
  8. Go语言的接口更小,只倾向于定义一个单一的动作,更有利于使用组合来复用代码
  9. Go语言拥有现代化的垃圾回收机制,显著降低了开发难度

Go开发注意事项

  1. 每个可执行的Go程序都有两个明显的特征,一个是作为程序入口的main函数,第二个是包含main函数的包名main。如果main函数不在main包里,构建工具不会生成可执行的文件
  2. Go语言的每个代码文件都属于一个包,一个包定义一组编译过的代码,类似于命名空间,可以把不同包中定义的同名标识符区别开
  3. 所有处于同一个文件夹内的代码文件必须使用同一个包名,按照惯例,包和文件夹同名
  4. 导入包时使用下划线是为了让Go语言对包做初始化操作,但是并不使用包里的标识符,因为Go不允许声明导入某个包但是不使用,下划线让编译器接受这类导入并且调用对应包里的所有代码文件里定义的init函数
  5. 程序中的每个代码文件里的init函数都会在main函数执行前调用
  6. 从标准库中导入代码时只需要给出要导入的包名,编译器总是会到GOROOT和GOPATH环境变量引用的位置去查找
  7. Go语言包里公开的标识符以大写字母开头,不公开的标识符以小写字母开头,不公开的标识符不能被其他包里的代码直接访问,但是可被间接访问
  8. Go语言中所有变量都被初始化为其零值,对于map、slice等引用类型,所引用的底层数据结构会被初始化为其对应的零值,但是被声明为其零值的引用类型的变量,会返回nil作为其值
  9. 通道与映射(map)和切片(slice)一样都是引用类型,不过通道本身实现的是一组带类型的值,这组值用于在goroutine之间传递数据
  10. Go语言中如果main函数返回,整个程序也就终止,程序终止时还会关闭所有值钱启动且还在运行的goroutine,因此在编写并发程序时最佳做法是在main函数返回前清理并终止所有值钱启动的goroutine
  11. Go语言中可以使用sync包的WaitGroup跟踪所有启动的goroutine,这是一个信号量,可以利用它来统计所有的goroutine是不是都完成了工作
  12. 如果要调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略掉。
  13. 查找map里的键时,要么赋值给一个变量,要么赋值给两个变量。赋值给两个变量时第一个值和赋值给第一个变量时的值一样,是map查找的结果值,如果指定了第二个值就会返回一个布尔标志来表示查找的键是否存在于map里。如果这个键不存在,map会返回其零值作为返回值,若存在则返回其副本
  14. Go语言中所有的变量都以值的方式传递,指针变量可以方便地在函数之间共享数据,使用指针变量可以让函数访问并修改一个变量的状态
  15. Go语言支持闭包,可以直接访问到那些没有作为参数传递的变量,闭包技术通过直接访问外层作用域的变量本身来进行访问。因为在循环迭代过程中不宜使用闭包,因为随着外层函数变量的改变,内层函数也会感知到这些改变,所有的goroutine都会因为闭包共享同样的变量
  16. Go编译器可以根据赋值运算符右边的值来推导类型,因此声明常量时不需要指定类型
  17. 解析JSON格式文档定义结构体类型时,每个字段声明的最后`引号里的部分被称作标记(tag),这个标记里描述了JSON解码的元数据,每个标记将结构类型字段对应到JSON文档里指定名字的字段
  18. 关键字defer会安排随后的函数调用在函数返回时才执行。哪怕函数意外崩溃终止,也能保证关键字defer安排调用的函数会被执行
  19. 命名接口时,如果接口类型只包含一个方法,那么按照Go语言的命名惯例,这个类型的名字以er结尾
  20. 空结构在创建实例时不会分配任何内存,这种结构很适合创建没有任何状态的类型
  21. 声明方法中的接受者时,如果为值本身,则可以通过值或者指向这个类型值的指针来调用该方法,因为编译器都会正确地引用或解引用对应的值
  22. 大部分方法在被调用后都需要维护接收者的值的状态,所以最佳实践是将方法的接收者声明为指针
  23. 使用指针作为接收者的方法,只能在接口类型是指针的时候被调用,使用值作为接收者声明的方法,在接口类型的值为值或者指针时都可以被调用
  24. 满足接口的类型的值和指针都可以作为接口类型的值,赋值和传递给接受接口类型值的函数
  25. 使用for...range循环遍历通道时,通道会一直被阻塞,直到有结果写入,一旦通道被关闭,for循环就会终止
  26. Go语言中可以直接使用取地址符(&)获取直接使用字面声明方式的新值的地址,如&point{x:1,y:2}

打包和工具链

  1. 编译器会使用Go环境变量设置的路径,通过引入的相对路径来查找磁盘上的包,标准库中的包会在安装Go的位置找到,Go开发者创建的包会在GOPATH环境变量指定的目录里查找
  2. 编译器会首选查找Go的安装目录,然后才会按顺序查找GOPATH变量里列出的目录
  3. Go工具链支持从网站获取源代码,会使用导入路径确定需要获取的代码在网络的什么位置
  4. Go工具链从分布式版本控制系统(DVCS)获取包,并把包的源代码保存在GOPATH指向的路径里与URL匹配的目录里。go get将获取任意指定的URL的包,或者一个已经导入包所依赖的其他包
  5. 当导入的多个包具有相同的名字时,重名的包可以通过命名导入来导入,在import语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字
  6. go buildgo clean命令会执行编译和清理可执行文件的工作,在不包含文件名时,go工具会默认使用当前目录来编译
  7. go run命令会编译并执行所创建的可执行程序
  8. go vet命令可以检测代码的常见错误,比如:
    (1)Printf类函数调用时,类型匹配错误的参数
    (2)定义常用的方法时,方法签名的错误
    (3)错误的结构标签
    (4)没有指定字段名的结构字面量
  9. go fmt命令会将代码布局成和Go源代码类似的风格
  10. go doc命令可以在终端上直接打印文档,而godoc命令则可以启动一个web服务器,通过浏览器的方式来查看Go语言的包的文档
  11. 当用户定义自己包的文档时,如果想要与标准库文档一样显示,首先需要在标识符之前把自己想要的文档作为注释加入到代码中,注释与C语言注释方式相同。如果想要给包写一段文字量比较大的文档,可以在工程里包含一个叫做doc.go的文件,使用同样的包名,并把包的介绍使用注释加在包名声明之前
  12. 将自己开发的包与其他开发者共享时需要注意以下规则:
    (1)包应该在代码库的根目录中
    (2)包可以非常小
    (3)对代码执行go fmt
    (4)给代码写文档
  13. 推荐使用依赖管理工具来管理依赖,有很多社区开发的依赖管理工具,如godepvendorgb

数组、切片和映射

  1. 声明数组时需要指定内部存储的数据的类型以及数组的长度,一旦声明,数据类型与长度就都不能改变了
  2. 如果使用...替代数组的长度,Go语言会根据初始化时数组元素的数量来确定该数组的长度
  3. 在Go语言里数组是一个值,同样类型的数组可以赋值给另一个数组,数组类型包括数组长度和每个元素的类型
  4. 复制指针数组,只会复制指针的值,而不会复制指针所指向的值
  5. 数组本身只有一维,但是可以组合多个数组创建多维数组,多维数组的类型包括每一维的长度以及最终存储在元素中的数据类型,并且可以独立复制某个维度
  6. 函数间传递数组总是以值的方式进行传递,如果数组本身很大会占用过多内存,因此一般传递数组指针的方式来传递大数组
  7. 切片是对数组的一种抽象,并提供相关的操作方法,包含指向底层数组的指针、切片访问的元素个数(长度)以及切片允许访问到的元素个数(容量)三个字段
  8. 创建切片时如果只指定长度,那么切片的容量和长度相等,也可以分别指定长度和容量,不允许创建容量小于长度的切片
  9. 当声明时不做初始化就会创建一个nil切片,如var slice []int,利用初始化也可以创建空切片,如slice:=make([]int,0)
  10. 对底层数组容量是k的切片slice[i:j]来说
    长度:j-i
    容量:k-i
  11. 当两个切片共享同一个底层数组时,如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到
  12. 切片只能访问到其长度内的元素,与切片容量相关联的元素只能用于增长切片,在使用这部分元素前,必须将其合并到切片的长度里
  13. 可以使用append函数将元素添加到切片中,该函数总是会增加新切片的长度,而容量有可能发生改变,有可能不变,这取决于被操作的切片的可用容量
  14. 创建切片时还可以使用第三个索引选项,这个选项可以用来控制新切片的容量,如slice:=a[i:j:k],此时切片长度和容量的计算方式如上文10中所述
  15. 如果试图设置的容量比可用容量还大,就会得到一个语言运行时错误
  16. 如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分离,新切片与原有的底层数组分离后,可以安全地进行修改
  17. append函数是一个可变参数的函数,如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里
  18. 使用for range循环遍历切片时,range创建了每个元素的副本,而不是直接返回对该元素的引用,lencap函数可以返回切片的长度和容量
  19. 由于切片的尺寸很小,因此在函数间复制和传递切片成本也很低
  20. 映射用于存储一系列无序的键值对,无序的原因是映射的实现使用了散列表
  21. 映射的键可以是任何值,只要这个值可以使用==运算符做比较,切片、函数以及包含切片的数据结构类型由于具有引用语义,不能作为映射的键
  22. Go语言里通过键来索引映射时,即便这个键不存在也总会返回一个值,此时返回的是该值对应的类型的零值
  23. 如果想把一个键值对从映射里删除,就使用内置的delete函数,在函数间传递映射传递的是引用,而不是副本,因此任何对映射所做的修改都会被感知

Go语言的类型系统

  1. 使用关键字struct可以让用户创建一个结构类型,结构类型通过组合一系列固定且唯一的字段来声明
  2. 创建一个变量并初始化为其零值,习惯使用关键字var,初始化为非零值,则配合结构字面量和短变量声明操作符来创建变量
  3. 结构字面量可以对结构类型采用两种形式,一种是声明每个字段的名字以及对应的值,子段名与值使用冒号分开,每一行以逗号结尾,这种形式对于字段声明顺序没有要求,另一种形式没有字段名,只声明对应的值,此时值的顺序必须要和结构声明中字段的顺序一致
  4. 另一种声明用户定义类型的方法是基于一个已有的类型,将其作为新类型的类型说明,比如type Duration int64,但是Go认为这两种类型是完全不同的两种类型,编译器不会对不同类型的值做隐式转换
  5. Go语言里方法有两种类型的接收者:值接收者和指针接收者,当以值调用指针接收者时编译器会自动获取当前值的地址,当以指针调用值接收者时编译器会自动获取指针指向值的副本
  6. 是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定,应该基于该类型的本质,除了需要让类型值符合某个接口时,一般选择使用值接收者声明方法
  7. 接口是用来定义行为的类型,被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现
  8. 方法集定义了一组关联到给定类型的值或者指针的方法,T类型的值的方法集只包含值接收者声明的方法,而指向T类型的只针对额方法集既包含值接收者声明的方法,又包含指针接收者声明的方法
  9. 上文8中的限制主要原因在于编译器并不是总能获得一个值的地址,比如临时字面量的值,如&int(10)就无法获取地址,所以值的方法集只包括了使用值接收者实现的方法
  10. 嵌入类型是将已有的类型直接声明在新的结构类型里,被嵌入的类型被称为新的外部类型的内部类型
  11. 通过嵌入类型,与内部类型相关的标识符会提升到外部类型上,这些被提升的标识符就像声明在外部类型里的标识符一样,也是外部类型的一部分,外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法
  12. 要嵌入一个类型,只需要声明这个类型的名字就可以了,外部类型可以使用内部类型的类型名来访问到内部类型的值,也可以直接通过外部类型的值来访问内部类型的标识符
  13. 由于内部类型的提升,内部类型实现的接口会自动提升到外部类型,但是当外部类型实现了相同的接口,内部类型的实现将不会被提升,但是内部类型的值一直存在,因此还可以通过直接访问内部类型的值来调用没有被提升的内部类型实现的方法
  14. 永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做
  15. 当内部类型是未公开的时,就无法直接通过结构字面量初始化该内部类型,但是即便其类型是未公开的,但是由于内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问