Go语言实战-1-5章
1.2 章 关于 Go 语言的介绍和快速开始一个 Go 程序
- 如果 main 函数不在 main 包中,构建工程就不会生成可执行文件。
- 程序中每个代码文件的 init 函数都会在 main 函数执行前调用。这个 init 函数将标准库里日志类的输出,从标准的默认错误(stderr),设置为标准输出(stdout)设备。
1 |
|
编译器查找包的时候,总是会到 GOROOT 和 GOPATH 环境变量。
map 是 Go 语言的一个引用类型,需要使用 make 来构造。如果不先构造 map 并将构造后的值赋值给变量,会在试图使用这个 map 变量时收到出错细节。这是因为 map 变量默认的零值是 nil。
如果需要声明初始值为零值的变量,应该使用 var 关键字声明变量。
匿名函数是指没有明确声明名字的函数
Go 语言支持闭包,这里就应用了闭包。实际上,在匿名函数内访问 searchTerm 和 results
变量,也是通过闭包的形式访问的。因为有了闭包,函数可以直接访问到那些没有作为参数传入
的变量。匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量
本身。因为 matcher 和 feed 变量每次调用时值不相同,所以并没有使用闭包的方式访问这两
个变量1
2
3
4go func(matcher Matcher,feed *Feed){
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher,feed)使用关键字 defer ,可以保证这个函数一定会被调用。哪怕函数意外崩溃终止,也能保证关键字 defer 安排调用的函数会被执行。
json 的一系列方法
1
2
3
4
5var feeds []*Feed
// 我们调用 json 包的 NewDecoder 函数,然后在其返回值上调用Decode 方法,调用 Decode 方法传入了切片地址
err = json.NewDecoder(file).Decode(&feeds)
return feeds,err1
func (dec *Decoder) Decode(v interface{}) error
这是 Decode 方法的声明。Decode 方法接受一个类型为 interface{}的值作为参数。这个类型在 Go 语言里很特殊,一般会配合 reflect 包里提供的反射功能一起使用。
命名接口的时候,也需要遵守 Go 语言的命名惯例。如果接口类型只包含一个方法,那么这个类型的名字以 er 结尾。
因为大部分方法在被调用后都需要维护接收者的值的状态,所以,一个最佳实践是,将方法的接收者声明为指针,使用指针可以在函数或者 goroutine 之间共享数据。而使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。
3. 打包和工具链
- 每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。这意味着,同一个目录下的所有.go 文件必须声明同一个包名。
- 所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。当编译器发现某个包的名字为 main 时,它一定也会发现名为 main()的函数,否则不会创建可执行文件。main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。程序编译时,会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。
- main函数必须放在main包里
- 标准库中的包会在安装 Go 的位置找到。Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。GOPATH 指定的这些目录就是开发者的个人工作空间。
- 有一件重要的事需要记住,编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录。
- go get 将获取任意指定的 URL 的包,
- 下划线 _ 在 Go 语言中称为空白标识符
- go clean xx.go ,调用 clean 会删除编译生成的可执行文件
- go build 可以在指定包的时候使用通配符
- go run 可以完成编译和运行两个操作,go run 命令会先构建 wordcount.go 里包含的程序,然后执行构建后的程序。这样可以节省好多录入工作量。
- fmt 工具会将开发人员的代码布局成和 Go 源代码类似的风格,不用再为了大括号是不是要放到行尾,或者用 tab(制表符)还是空格来做缩进而争论不休。使用 go fmt 后面跟文件名或者包名,就可以调用这个代码格式化工具。
good -http=:6060
可以实现在 6060 端口访问 go 的文档,如果浏览器已经打开,导航到http://localhost:6060 可以看到一个页面,包含所有 Go 标准库和你的 GOPATH 下的 Go 源代码的文档。
显示结果为:
4. 数组,切片和映射
如果使用…替代数组的长度,Go 语言会根据初始化时数组元素的数量来确定该数组的长度
声明数组指定位置的值:
array := [5]int{1: 10, 2: 20}
,表明数组大小是 5,下标为 1 的位置的元素的值是 10,下标为 2 的位置的元素的值是 20。数组变量的类型包括数组长度和每个元素的类型。
二维数组的赋值:
aray := [4][2]{{10, 11},{10, 11},{10, 11},{10, 11}}
声明一个需要 8 MB 的数组:var array [1e6]int
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。
一种创建切片的方法是使用内置的 make 函数。当使用 make 时,需要传入一个参数,指定切片的长度,如果只指定长度,那么切片的容量和长度相等。如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。
例子:创建切片
s:=[]string{"1","2}
,使用空字符串初始化第100个元素,slice:=[]string{99:""}
创建 nil 整形切片:
var slice []int
使用 make 创建空的整型切片:
slice := make([]int, 0)
。使用切片字面量创建空的整型切片:slice := []int{}
。空切片在底层数组包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用,例如,数据库查询返回 0 个查询结果时。切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分。
对底层容量是 k 的切片 slice[i:j] 来说,长度j-i,容量:k-i
基于切片创建切片:
1 |
|
切片有额外的容量是很好,但是如果不能把这些容量合并到切片的长度里,这些容量就没有用处。好在可以用 Go 语言的内置函数 append 来做这种合并很容易。
如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值
函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。随着语言的演化,这种增长算法可能会有所改变。
对于
slice[i:j:k]或者[2:3:4]
,其长度为 j-i 或者 3-2 =1,其容量为 k-i 或者 4-2=2。如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改,
Go 语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的元素。
和数组一样,切片是一维的。不过,和之前对数组的讨论一样,可以组合多个切片形成多维切片,比如可以类似二维数组一样创建二维切片。
将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组。函数调用时复制切片,函数返回时复制切片。
切片每次迭代映射的时候顺序也可能不一样。无序的原因是映射的实现使用了散列表,映射通过合理数量的桶来平衡键值对的分布。
切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。
可以通过声明一个未初始化的映射来创建一个值为 nil 的映射(称为 nil 映射 )。nil 映射不能用于存储键值对,否则,会产生一个语言运行时错误,比如
var s map[string]string s["a"]="a"
会报错:assignment to entry in nil map
。如果想把一个键值对从映射里删除,就使用内置的 delete 函数,比如 delete(mapvalue, key)
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。也就是类似指针,一个地方改了其他地方有会改。
5. Go 语言的系统类型
Go 是一种静态类型的语言。
bool 类型的值需要 1 字节(8 位),表示布尔值 true和 false。有些类型的内部表示与编译代码的机器的体系结构有关。例如,根据编译所在的机器的体系结构,一个 int 值的大小可能是 8 字节(64 位),也可能是 4 字节(32 位)。
Go 语言里声明用户定义的类型有两种方法。最常用的方法是使用关键字 struct,它可以让用户创建一个结构类型。
任何时候,创建一个变量并初始化为其零值,习惯是使用关键字 var。
type Duration int64
,在 Duration类型的声明中,我们把 int64 类型叫作 Duration 的基础类型。不过,虽然 int64 是基础类型,Go 并不认为 Duration 和 int64 是同一种类型。这两个类型是完全不同的有区别的类型。方法能给用户定义的类型添加新的行为。方法实际上也是函数。
关键字 func 和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为方法。
Go 语言里有两种类型的接收者:值接收者和指针接收者。
如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。
Go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型。这个支持非常方便开发者编写程序。
如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。
内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
Go语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称作标头(header)值。从技术细节上说,字符串也是一种引用类型。每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。
一个值接收者,正像预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。这种传递方法也可以应用到函数或者方法的参数传递。
当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。
Chdir 方法展示了,即使没有修改接收者的值,依然是用指针接收者来声明的。因为 File 类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。
1
2
3
4func (f *File)Chdir()error{
// do some read
return nil
}http.Response 类型包含一个名为 Body 的字段,这个字段是一个 io.ReadCloser 接口类型的值。Body 字段作为第二个参数传给 io.Copy 函数。io.Copy 函数的第二个参数,接受一个 io.Reader 接口类型的值,这个值表示数据流入的源。
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。因为任何用户定义的类型都可以实现任何接口,所以对接口值方法的调用自然就是一种多态。在这个关系里,用户定义的类型通常叫作实体类型,原因是如果离开内部存储的用户定义的类型的值的实现,接口值并没有具体的行为。
接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。
方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。
Go 语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
一旦我们将 user 类型嵌入 admin,我们就可以说 user 是外部类型 admin 的内部类型。由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。比如
1 |
|
如果外部类型实现了 do 方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。
- 将工厂函数命名为 New 是 Go 语言的一个习惯。比如 New 函数做了些有意思的事情:它创建了一个未公开的类型的值。要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。