Raymond's Notebook
日拱一卒无有尽,功不唐捐终入海
golang 学习笔记

接触 Golang 有几年了,做过几个web应用,看过 《The Little Go Book》。但是总感觉所知甚浅,直到看了《The Go Programming Language》这本经典,才感觉对 Golang 有了相对深入的理解。 以下是我学习Golang时的学习笔记,希望对想要学习这门语言的同学有所帮助。

基础知识点

  • 未被使用的包或未使用的变量 都是不允许的,会导致编译不通过。
  • 可以是用 var NAME TYPE 来声明变量,并且变量的初始值为它相应类型的零值,声明变量时可以不指定变量类型(因为支持自动类型推断);或者使用 NAME := VALUE 声明并赋值变量,可以一次对多个变量赋值。有一点区别,NAME := VALUE 只能在函数内部使用,不能作为包级变量声明。
  • 函数天生支持多返回值,如果想丢弃函数的某个返回值,可以使用 _ 空白标识符。
  • go 不是面向对象的语言,它没有对象和继承的概念。不过它提供了 struct ,试图通过组合来实现继承。
  • go 语言中,函数的参数传递都是按值传递,即传递的是一个拷贝。不管传递的是值还是地址,均是前值的一个copy。
  • 由于在go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
  • struct 为用户提供了自定义类型的途径,尽管它没有构造函数,但是却有一个内置函数 new , 可以用来分配一个类型需要的内存,并且 new(X) 和 &X{} 是等效,它们均返回指向内存的指针。
  • package 包就是包含了代码文件的目录 -> A directory with some code files。
  • 当你命名一个包时,通过使用关键字 package,你只需要提供单个值,而不是一个完整的层次结构;但是当你导入一个包时,你需要指定一个全路径。
  • 引用其他package的变量,函数,struct ,需要在前面写上包名前缀。
  • 可见性,如果你命名的类型或者函数是以一个大写字母开头,那么这个类型或者函数就是对外可见的;如果使用一个小写字母开头,那么就是不可见的。
  • channel 的零值为 nil,如果在 nil 通道上发送和接收将永远阻塞。在一个 empty 的 channel 上读取和压入也会阻塞当前 goroutine。
  • select 工作机制:1, 第一个可用的channel被选中,执行其分支语句;2,如果多个通道可用,随机选中一个通道执行;3,如果没有通道可用,default 分支立刻被执行;如果也没有default分支,select将被阻塞。执行完分支语句后,select 将退出,因此为了让 select 一直监控 channel,select 经常和 for 配合使用。
  • 表达式有返回值,但是语句没有返回值。i++ 在golang中是一条语句(等效于: i = i +1) 所以j = i++非法,而且++和–都只能放在变量名后面,因此–i也非法。
  • 赋值不是表达式,是语句,意味着赋值不会有返回值。
  • 字符串的零值为空字符串””,如果你声明但是不初始化(赋值)一个字符串变量,那么这个变量的值就是空字符串,而不是nil,更不是 null。
  • 内存以零值初始化任何数据。
  • 内建string、slice和map。
  • switch case 语句默认不能贯穿执行; 可以用 fallthrough 改变这种行为。
  • 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。
  • 两个数组的类型一样的前提是:1,数组元素类型一样,2,数组长度一样。只有在保证数组类型一样的情况下,两个数组才能进行比较,如果两个数组的元素相等,且类型一致,数组才相等。
  • go 中数组是固定大小的;但是 slice 的长度却是可变的,它表示一个拥有相同类型元素的可变长度的序列;通常写成 []T,其中元素类型是 T。有个特性需要注意,slice 只是一个到数组的引用。
  • 一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
  • slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。如果append函数导致了slice的扩容,那么slice默认会扩容成之前slice容量的2倍,会新分配一个存储空间,将之前slice的元素copy过去,然后将append的内容,追加到新空间的后面。如果slice来源于数组,那么数组的长度永远不会受到影响,也就是slice的扩容会分配新的空间,不会改变原有数组的大小。
  • 和数组不一样,slice 之间不支持比较运算符,不能直接比较。
  • Map的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断key是否已经存在于map当中。也就意味着 slice 是不能做为 key 的。
  • 在向map存数据前必须先创建map, 如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值;因此判断key是否存在就不能依赖于其对应的value,正确的做法是使用如下这样的语句:if _, ok := mp[“foo”]; ok {}
  • 和slice一样,map之间也不能进行相等比较;唯一的例外是可以和nil进行比较.
  • slice ,map 均为引用类型,类似于指针。
  • 结构体类型的零值,是每个成员都是零值。
  • 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。
  • go语言有一个特性让我们只声明一个成员对应的数据类型(自定义结构体类型)而不指定成员的名字;这类成员就叫匿名成员,得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径。
  • 简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。这个机制可以用于将一些有简单行为的对象组合成有复杂行为的对象。组合是go语言中面向对象编程的核心。相当于实现了混入Mix,类似 Ruby模块混入。
  • 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。
  • go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑栈溢出和安全问题。
  • 在go的panic机制中,延迟函数的调用在释放堆栈信息之前。
  • 当向一个关闭的 channel 发送数据时,将导致 panic;但是在一个关闭的 channel 上面接收数据时,等先前的数据接收完毕,后续的接收操作将不再阻塞,它们会立即返回一个零值。
  • 通过 for {} 循环读取 channel 中的值和使用 range 循环读取值之间的区别在于:用 for,当 channel 被关闭时,不会自动退出循环;但是 range 在channel被关闭并且没有值可以接收时,会自动退出循环。
  • 不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾回收器自动回收。
  • chan<- int 只写channel,只能向它发送写入,不能接收; <-chan int 只读channel,只能从它接收,不能写入。
  • 只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误。
  • 任何双向channel向单向channel变量的赋值操作都将导致隐式转换;不能一个将类似chan<‐ int 类型的单向型的channel转换为chan int 类型的双向型的channel。

关于对象的方法调用

  • 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  • 在声明一个method的receiver是指针还是非指针类型时,你需要考虑两方面的问题,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝(因为只是地址COPY,两个不同的指针仍然指向同一片内存空间)。
  • T类型的参数上调用一个T的方法是合法的,只要这个参数是一个变量(但不能是临时变量);编译器隐式的获取了它的地址。 但这仅仅是一个语法糖:T类型的值不拥有所有*T指针的方法,那这样它就可能只实现更少的接口。
  • 接口类型描述了一系列方法的集合,任何实现了这些方法的类型,都是这个接口类型的实例。 一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。那么对于一个空接口来说 interface{},go中的其他所有类型都默认实现了空接口。在参数传递时,一个带空接口类型的函数,就可以接收任意类型的参数传入。

关于 golang env, GOPATH, GOROOT, go mod

  • 默认情况下golang env 里面的 GOPATH 默认是 /$HOME/go , 你安装完golang后,这个目录并没有创建。
  • 默认情况下golang env 里面的 GOROOT 默认是,golang 的安装目录。
  • 当你第一次使用 go mod tidy 或者 go build 或者 go run ,这个时候golang 会下载项目中的依赖,这时如果
    (a), 你没有设置自己的$GOPATH,那么就使用 /$HOME/go 作为GOPATH,由于默认这个目录没有创建,golang会自动创建这个目录,然后把下载的依赖放置到: /$HOME/go/pkg/mod 里面。
    (b), 如果你已经设置了自己的 $GOPATH,那么就把依赖放到 $GOPATH/pkg/mod 里面。
    也就是无论你是否设置 $GOPATH,最后第三方依赖都是放置到 GOPATH/pkg/mod 下面,只是如果你不设置,golang 会使用默认的GOPATH。
  • go mod 是golang 1.11 新加的特性,是go官方的包依赖管理工具,详细说明可以参考 这里