本学习笔记总结自: 《Go in Action》
先讨论一些规范问题:
- 如何将代码组织成包
- 如何操作这些包
包
有这么一些目录,目录下存放一些列以 .go
为扩展名的相关文件,这个目录,称之为包。
同一个目录下的所有 .go
文件必须声明为同一个包名,比如包为 beauty
,则需要在代码中标明 package beauty
。
main 包
所有用 Go 语言编译的可执行程序都必须有一个名叫 main
的包,当编译器找到 main
包,会去找寻 main()
函数,如果没有,则不会生成可运行二进制文件。
导入
导入一个包极其简单 —- import "beauty"
。注意,如果你导入了一个包,却不用,编译器会报错。
init 函数
如你在Golang 初探所看到的,在 matcher
中,你会看到 init
函数,如果你以这种方式 import _ "beauty"
导入包,那么会在程序开始运行时,执行这个包下面的 init
函数。
数组
使用
1 | // 声明一个长度为5的数组,其中的值初始化为0 |
在函数间直接传递数组是直接拷贝数组,如果数组过大,直接传递指针即可。通过 &
符号,可以获取指定数组的地址,如:&array1
。
切片
什么是切片
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念来构建的,可以按需自动增长和缩小。切片的动态增长是通过函数 append
来实现的。同时,切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
实现原理
使用
1 | // 声明一个长度、容量都为5的切片 |
1 | slice := []int{1, 2, 3, 4, 5} |
通过一张图片观察上面两行代码所做的事。
接着我们再通过一段代码来看如何对切片进行遍历:
1 | slice := []int{1, 2, 3, 4} |
需要注意的只有两点:
- 使用
range
函数获得的两个值,一个是索引,一个是索引对应的值。 range
其实是对每个元素提供了一个副本。
你可以通过下划线 _
来忽略函数返回的值。
1 | slice := []int{1, 2, 3, 4} |
那么如何在函数中传递切片呢?
1 | slice := make([]int, 1e6) |
- 在一个64位的机器上,一个切片需要24字节的内存:指针字段8字节、长度和容量分别需要8字节
- 函数中传递时仅复制切片本身,不复制底层数组
映射
什么是映射
映射是一种数据结构,用于存储一系列无序的键值对。
实现
映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,所有操作都要先选择一个桶。把操作映射时制定的键传给映射的散列函数,就能选中对应的桶。
散列函数的目的是生成一个索引,这个索引将最终将键值对分布到所有可用的桶里。
使用
先看怎么创建、初始化
1 | dict := make(map[string]int) |
只需要注意一点,作为键的类型,可以是内置的类型,也可以是结构类型,只要这个类型可以使用 ==
来比较。
1 | value, exists := colors["Blue"] |
通过这种方式,可以判断是否存在需要的键值对。
1 | colors := map[string]string { |
通过上述代码,我们可以轻松遍历 map 中的键值对,同样,你可以使用 _
符号来忽略函数的某个/所有返回值。
Golang 中删除映射中的键值对也是很方便的。
1 | colors := map[string]string { |
那么如何在函数中传递使用映射呢?
1 | func main() { |
通过运行上述代码,我们可以发现,在调用了 removeColor
函数之后再遍历映射,会发现被删除的元素不存在了。这其中的道理很简单,传递映射时并没有对其进行复制,使用的底层数组仍然是同一个。
类型系统
Golang 是一种静态编译的语言 – 编译器需要在编译时知晓程序里每个值的类型,用于提供编译器对代码进行一些性能优化,提高执行效率。
值的类型给编译器提供两部分信息:
- 需要分配多少内存
- 这段内存表示什么
结构体
什么是结构体
Golang 允许用户定义类型,当用户声明一个新类型时,这个声明就给编译器提供了一个框架,告知必要的内存大小和表示信息。
声明
有两个方式声明结构体。
- 利用
struct
关键字
1 | type user struct { |
- 基于一个已有的类型,将其作为新类型的类型说明
1 | type Length int64 |
初始化
1 | yorr := user { |
嵌入类型: Golang 允许用户扩展或者修改已有类型的行为,这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。通过
嵌入类型
完成。
1 | package main |
以上我们注意到一个函数 – notify
。可以给我们引入一个新的话题 —— 值接收者、指针接收者。
值接收者&指针接收者
1 | func (u user) notify() { |
notify
和 changeEmail
称为结构体 user
的方法,如果一个函数有接收者,这个函数就被称为方法。在 func
关键字和方法名之间的参数,称之为 接收者。而接收者分为两类,值接收者和指针接收者。
1 | package main |
我们可以看到,bill
作为一个值,可以调用值接收者声明的方法,也可以调用指针接受者声明的方法,其实里面有一个语法糖。在用值对象调用指针接收者声明的方法时,golang 底层做了这样一个操作 (&bill).changeEmail()
,同理,指针对象调用值接收者声明的方法时,做了这样的操作 (*lisa).notify()
。
使用值接收者方法时,实际上会创建一个对象的副本,指针接收者则是利用指针,如果修改,会直接修改结构体里对应的项。
内置类型
内置类型是由语言提供的一组类型。
如: 数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型,因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个理论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
1 | func Trim(s string, cutset string) string { |
Trim
函数传入一个 string
类型的值作操作,在传入一个 string
类型的值用于查找,之后函数会返回一个新的 string
类型的值作操作。
引用类型
Golang 中,引用类型有如下几个: 切片、映射、通道、接口和函数。
当声明上述类型变量时,创建的变量被称作标头值,从技术细节上说,字符串也是一种引用类型。
每个引用类型创建的标头值是包含一个指向底层数据结构的指针,每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的副本,本质上就是在共享底层数据结构。
同时,编译器只允许为命名的用户定义的类型声明方法。
接口
什么是接口
接口定义了一个操作。
实现
讨论如何实现一个接口。
接口定义的类型不由接口直接实现,而是通过方法由用户定义的类型实现。
如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
1 | package main |
简单介绍一下方法集规则:
Values | Methods Receivers |
---|---|
T | (t T) |
* T | (t T) and (t *T) |
Methods Receivers | Values |
---|---|
(t T) | T and *T |
(t * T) | *T |
我们可以发现,在上面代码中,有这样两行代码:
1 | sendNotification(&u) |
按照我们之前讨论的语法糖,也可以这么写:
1 | sendNotification(u) |
但是实际上,这样做是不可以的。通过方法集规则的描述,我们发现,我们不是总能自动获得一个值的地址。所以值的方法集只包括了使用值接收者实现的方法。
多态
在了解完接口后,我们来看一下如何通过使用接口来实现多态。
1 | package main |
通过多态函数 sendNotification
,我们实现了多态目标。
公开和私有
说完了基本的语法,我们要说一下方法、字段、等等一些内容的公开化和私有化问题。如果你学过 Java
或者 C++
之类的面向对象语言。你会看到过这样的关键字 public
和 private
。
而对于 golang
是没有这些玩意的。
那 golang
如何实现公开或者私有呢?
开头字母大小写!
如果开头字母是大小,表示对其他 package
是公开的标识。反之,对其他 package
则不是。
需要注意的是,无论是大写还是小写,在文件自己待的 package(目录)
下,对于同级别的 package(目录)
都是公开的。