LinMao's Blog
学习科研记录与分享!

GO语言学习笔记

根据网上资料整理一些重点,当作是笔记,借此对GO语言基本内容形成一个网络。

Go 环境变量

  • $GOROOT 表示 Go 在电脑上的安装位置,它的值一般都是 $HOME/go,当然,也可以安装在别的地方。
  • $GOARCH 表示目标机器的处理器架构,它的值可以是 386、amd64 或 arm。
  • $GOOS 表示目标机器的操作系统,它的值可以是 darwin、freebsd、linux 或 windows。
  • $GOBIN 表示编译器和链接器的安装位置,默认是 $GOROOT/bin,如果使用的是 Go 1.0.3 及以后的版本,一般情况下可以将它的值设置为空,Go 将会使用前面提到的默认值。
  • $GOPATH 默认采用和 $GOROOT 一样的值,但从 Go 1.1 版本开始,必须修改为其它路径。它可以包含多个 Go 语言源码文件、包文件和可执行文件的路径,而这些路径下又必须分别包含三个规定的目录:srcpkgbin,这三个目录分别用于存放源码文件、包文件和可执行文件。
  • $GOARM 专门针对基于 arm 架构的处理器,它的值可以是 5 或 6,默认为 6。
  • $GOMAXPROCS 用于设置应用程序可使用的处理器个数与核数。

GO语言特性

package

所有的包名都应该使用小写字母。如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。

可见性:当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

关键字

GO语言很多语法特点和C语言非常相似,这里主要记录一些不一样的,以供查阅。

Go 代码中会使用到的 25 个关键字或保留字:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数。

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

基本数据类型

GO的基本数据类型以及相关运算与C/C++大致相似。

String类型的操作定义在strings package,可查阅相关函数

时间相关操作定义在time package,可以查阅相关函数

分支结构

if-else

关键字 if 和 else 之后的左大括号 { 必须和关键字在同一行,如果使用了 else-if 结构,则前段代码块的右大括号 } 必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。

switch

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。

可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3

每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。( Go 语言使用快速的查找算法来测试 switch 条件与 case 分支的匹配情况,直到算法匹配到某个 case 或者进入 default 条件为止。)

一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说不需要特别使用 break 语句来表示结束。

因此,程序也不会自动地去执行下一个分支的代码。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。

for

基本形式1

for 初始化语句; 条件语句; 修饰语句 {}

基本形式2,基于条件判断,类似于while

for 条件语句 {}

for range

基本形式

for ix, val := range collection { }

val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值.。如果 val 为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)。一个字符串是 Unicode 编码的字符(或称之为 rune)集合,因此也可以用它迭代字符串。

str := "编程语言"
for pos, val := range str {
    fmt.Printf("%d: %c\n", pos, val)
}
// output
0: 编
3: 程
6: 语
9: 言

goto

goto有点像汇编里面的跳转指令。

 

GO函数

参数

Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)。如果希望函数可以直接修改参数的值,而不是对参数的副本进行操作,需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。

几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

传递变长参数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。这个函数接受一个类似某个类型的 slice 的参数。

func myFunc(a, b, arg ...int) {}

 

返回值

getX2AndX3getX2AndX3_2 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int)

命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来。即使函数使用了命名返回值,依旧可以无视它而返回明确的值。

// 以下两个function作用一样
func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
}

对于多返回值的函数,可以使用_舍弃返回值。

defer关键字

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)。

关键字 defer 允许我们进行一些函数执行完成后的收尾工作。

// open file
defer file.Close()

内置函数

名称说明
close用于管道通信
len、caplen 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、makenew 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作。new() 是一个函数,不要忘记它的括号。
copy、append用于复制和连接切片
panic、recover两者均用于错误处理机制
print、println底层打印函数,在部署环境中建议使用 fmt 包
complex、real imag用于创建和操作复数

函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。如下:

func main() {
	callback(1, Add)
}

func Add(a, b int) {
	fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
	f(y, 2) // this becomes Add(1, 2)
}

闭包

当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int { return x + y }

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fplus(3,4)

当然,也可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4)

 

数组与切片

数组

数组的声明格式:

var identifier [len]type

当声明数组时所有的元素都会被自动初始化为默认值 0,长度为len(identifier)

Go 语言中的数组是一种 值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建: var arr1 = new([5]int)。这种方式和 var arr2 [5]int 的区别是:arr1 的类型是 *[5]int,而 arr2的类型是 [5]int

当把一个数组赋值给另一个时,需要再做一次数组内存的拷贝操作。例如:

arr2 := *arr1
arr2[2] = 100

这样两个数组就有了不同的值,在赋值后修改 arr2 不会对 arr1 生效。如果想修改原数组,那么 arr2 必须通过&操作符以引用方式传过来,例如 func1(&arr2),下面是一个例子

带初始值初始化的声明

第一种

var arrAge = [5]int{18, 20, 15, 22, 16}

注意 [5]int 可以从左边起开始忽略:[10]int {1, 2, 3} :这是一个有 10 个元素的数组,除了前三个元素外其他元素都为 0。

第二种

var arrLazy = [...]int{5, 6, 7, 8, 22}

... 可同样可以忽略,从技术上说它们其实变化成了切片。

第三种(key: value 语法)

var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}

只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串。在这里数组长度同样可以写成 ...

切片

切片(slice)是对数组一个连续片段的引用(该数组称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。

切片是可索引的,并且可以由 len() 函数获取长度。给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组

切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:0 <= len(s) <= cap(s)

多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。数组实际上是切片的构建块。(可能存在data race)

因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。

切片声明格式:(不需要说明长度)

var identifier []type

一个切片在未初始化之前默认为 nil,长度为 0。切片的初始化格式是:

var slice1 []type = arr1[start:end]

这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start]。这可以在 arr1 被填充前就定义好。

  • var slice1 []type = arr1[:] 表示slice1 就等于完整的 arr1 数组(这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1
  • arr1[2:]arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。
  • arr1[:3]arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第四个)。
  • 一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3}[:](注: 应先用s := [3]int{1, 2, 3}生成数组, 再使用s[:]转成切片) 甚至更简单的 s := []int{1,2,3}

切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。如果 s2 是一个 slice,你可以将 s2 向后移动一位 s2 = s2[1:],但是末尾没有移动。切片只能向后移动,s2 = s2[-1:] 会导致编译错误。切片不能被重新分片以获取数组的前一个元素。不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!

将切片传递给函数

声明方式:

func sum(a []int) int {
	s := 0
	for i := 0; i < len(a); i++ {
		s += a[i]
	}
	return s
}

用 make() 创建一个切片

当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组:var slice1 []type = make([]type, len)。也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。make 接受 2 个参数:元素的类型以及切片的元素个数。

如果想创建一个 slice1,它不占用整个数组,而只是占用以 len 个数据项,那么只要:slice1 := make([]type, len, cap)

make 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数。

下面两种方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

new() 和 make() 的区别

二者都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

  • new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}
  • make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。

换言之,new 函数分配内存,make 函数初始化。

如何理解new、make、slice、map、channel的关系

  1. slice、map以及channel都是golang内建的一种引用类型,三者在内存中存在多个组成部分, 需要对内存组成部分初始化后才能使用,而make就是对三者进行初始化的一种操作方式
  2. new 获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作, 所以slice、map、channel需要make进行初始化并获取对应的内存地址,而非new简单的获取内存地址

bytes 包

类型 []byte 的切片十分常见,Go 语言有一个 bytes 包专门用来解决这种类型的操作方法。

bytes 包和字符串包十分类似。而且它还包含一个十分有用的类型 Buffer。这是一个长度可变的 bytes 的 buffer,提供 Read 和 Write 方法,因为读写长度未知的 bytes 最好使用 buffer。Buffer 可以这样定义:

var buffer bytes.Buffer
// 或者
var r *bytes.Buffer = new(bytes.Buffer)

通过 buffer 串联字符串

创建一个 buffer,通过 buffer.WriteString(s) 方法将字符串 s 追加到后面,最后再通过 buffer.String() 方法转换为 string。这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候。

var buffer bytes.Buffer
for {
	if s, ok := getNextString(); ok { //method getNextString() not shown here
		buffer.WriteString(s)
	} else {
		break
	}
}
fmt.Print(buffer.String(), "\n")

数组和切片中的for-range用法

for ix, value := range slice1 {
	...
}

第一个返回值 ix 是数组或者切片的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的局部变量。value 只是 slice1 某个索引位置的值的一个拷贝,不能用来修改 slice1 该索引位置的值。

如果只需要索引位置的值,可以用_来忽略索引:

for _, value := range slice1 {
	...
}

如果只要索引,可以直接忽略索引位置的值:

for ix := range seasons {
	fmt.Printf("%d", ix)
}

切片重组 reslicing

切片创建的时候通常比相关数组小,例如:

slice1 := make([]type, start_length, capacity)

其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。

这么做的好处是我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组(reslicing),做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。

将切片扩展 1 位:

sl = sl[0:len(sl)+1]

切片copy和append函数

如果想增加切片的容量,必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。

package main
import "fmt"

func main() {
	slFrom := []int{1, 2, 3}
	slTo := make([]int, 10)

	n := copy(slTo, slFrom)
	fmt.Println(slTo)
	fmt.Printf("Copied %d elements\n", n) // n == 3

	sl3 := []int{1, 2, 3}
	sl3 = append(sl3, 4, 5, 6)
	fmt.Println(sl3)
}

func append(s[]T, x ...T) []T 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽了。

func copy(dst, src []T) int copy 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 srcdst 的长度最小值。如果src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst

Map数据结构

map 是一种特殊的数据结构:一种元素对(pair)的无序集合,pair 的一个元素是 key,对应的另一个元素是 value,所以这个结构也称为关联数组或字典。这是一种快速寻找值的理想结构:给定 key,对应的 value 可以迅速定位。

map 是引用类型,可以使用如下声明:

var map1 map[keytype]valuetype
var map1 map[string]int

在声明的时候不需要知道 map 的长度,map 是可以动态增长的。未初始化的 map 的值是 nil。key 可以是任意可以用 == 或者 != 操作符比较的类型,比如 string、int、float。所以数组、切片和结构体不能作为 key(slice可以作为map的值) ,但是指针和接口类型可以。如果要用结构体作为 key 可以提供 Key()Hash() 方法,这样可以通过结构体的域计算出唯一的数字或者字符串的 key。

map 也可以用函数作为自己的值,这样就可以用来做分支结构,根据 key 用来选择要执行的函数。常用的 len(map1) 方法可以获得 map 中的 pair 数目,这个数目是可以伸缩的,因为 map-pairs 在运行时可以动态添加和删除。

map 是 引用类型 的: 内存用 make 方法来分配。

var map1 = make(map[keytype]valuetype)
// 或者
map1 := make(map[keytype]valuetype)

不要使用 new,永远用 make 来构造 map。

map容量

和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity

make(map[keytype]valuetype, cap)
map2 := make(map[string]float32, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

map判断键值对是否存在和删除键值对

判断键值对是否存在

val1, isPresent = map1[key1]	// if key1存在, isPresent==true; else is Present==false

// 和if混用方法
if _, ok := map1[key1]; ok {
	// ...
}

删除键值对

// 从map中删除key1, 若key1不存在不会产生错误
delete(map1, key1)

for-range用法

和在切片和数组中的用法一致。

map排序

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序,如果要为 map 排序,需要将 key(或者 value)拷贝到一个切片,再使用 sort 包对切片排序,然后可以使用切片的 for-range 方法打印出所有的 key 和 value。

包 package

常用标准库包

fmtos 等这样具有常用功能的内置包在 Go 语言中有 150 个以上,它们被称为标准库,大部分(一些底层的除外)内置于 Go 本身。

  • unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。

  • syscall-os-os/exec

    • os: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
    • os/exec: 提供我们运行外部操作系统命令和程序的方式。
    • syscall: 底层的外部包,提供了操作系统底层调用的基本接口。
    // 重启linux
    package main
    import (
    	"syscall"
    )
    
    const LINUX_REBOOT_MAGIC1 uintptr = 0xfee1dead
    const LINUX_REBOOT_MAGIC2 uintptr = 672274793
    const LINUX_REBOOT_CMD_RESTART uintptr = 0x1234567
    
    func main() {
    	syscall.Syscall(syscall.SYS_REBOOT,
    		LINUX_REBOOT_MAGIC1,
    		LINUX_REBOOT_MAGIC2,
    		LINUX_REBOOT_CMD_RESTART)
    }
    
  • archive/tar/zip-compress:压缩(解压缩)文件功能。

    • fmt-io-bufio-path/filepath-flag
    • fmt: 提供了格式化输入输出功能。
    • io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
    • bufio: 缓冲输入输出功能的封装。
    • path/filepath: 用来操作在当前系统中的目标文件名路径。
    • flag: 对命令行参数的操作。
  • strings-strconv-unicode-regexp-bytes

    • strings: 提供对字符串的操作。
    • strconv: 提供将字符串转换为基础类型的功能。
    • unicode: 为 unicode 型的字符串提供特殊的功能。
    • regexp: 正则表达式功能。
    • bytes: 提供对字符型分片的操作。
    • index/suffixarray: 子字符串快速查询。
  • math-math/cmath-math/big-math/rand-sort

    • math: 基本的数学函数。
    • math/cmath: 对复数的操作。
    • math/rand: 伪随机数生成。
    • sort: 为数组排序和自定义集合。
    • math/big: 大数的实现和计算。
  • container-/list-ring-heap: 实现对集合的操作

    • list: 双链表。
    • ring: 环形链表。
    // 下面代码演示了如何遍历一个链表(当 l 是 *List):
    for e := l.Front(); e != nil; e = e.Next() {
    	//do something with e.Value
    }
    
  • time-log

    • time: 日期和时间的基本操作。
    • log: 记录程序运行时产生的日志,我们将在后面的章节使用它。
  • encoding/json-encoding/xml-text/template:

    • encoding/json: 读取并解码和写入并编码 JSON 数据。
    • encoding/xml:简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
    • text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
  • net-net/http-html:

    • net: 网络数据的基本操作。
    • http: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。
    • html: HTML5 解析器。
  • runtime: Go 程序运行时的交互操作,例如垃圾回收和协程创建。

  • reflect: 实现通过程序运行时反射,让程序操作任意类型的变量。

锁和sync包

在 Go 语言中这种锁的机制是通过 sync 包中 Mutex 来实现的。sync 来源于 "synchronized" 一词,这意味着线程将有序的对同一变量进行访问。sync.Mutex 是一个互斥锁,它的作用是守护在临界区入口来确保同一时间只能有一个线程进入临界区。

假设 info 是一个需要上锁的放在共享内存中的变量。通过包含 Mutex 来实现的一个典型例子如下:

import  "sync"

type Info struct {
	mu sync.Mutex
	// ... other fields, e.g.: Str string
}

// 如果一个函数想要改变这个变量可以这样写
func Update(info *Info) {
	info.mu.Lock()
    // critical section:
    info.Str = // new value
    // end critical section
    info.mu.Unlock()
}

还有一个很有用的例子是通过 Mutex 来实现一个可以上锁的共享缓冲器:

type SyncedBuffer struct {
	lock 	sync.Mutex
	buffer  bytes.Buffer
}

在 sync 包中还有一个 RWMutex 锁:他能通过 RLock() 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 Lock() 将和普通的 Mutex 作用相同。包中还有一个方便的 Once 类型变量的方法 once.Do(call),这个方法确保被调用函数只能被调用一次。

相对简单的情况下,通过使用 sync 包可以解决同一时间只能一个线程访问变量或 map 类型数据的问题。如果这种方式导致程序明显变慢或者引起其他问题,我们要重新思考来通过 goroutines 和 channels 来解决问题,这是在 Go 语言中所提倡用来实现并发的技术。

自定义包

包是 Go 语言中代码组织和代码编译的主要方式。

定义第三方包的步骤:

  1. 首先使用go mod init xxx来初始化mod
  2. 在该project下面创建包,每个文件夹下面只有一种包
  3. 在其他文件中import包格式为import xxx/package_name

结构(struct)与方法(method)

结构

Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。

结构体也是值类型,因此可以通过 new 函数来创建。组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

结构体定义的一般方式:

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。

使用 new

使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行(比如定义是包范围的,但是分配却没有必要在开始就做)。

var t *T
t = new(T)

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。使用 fmt.Println 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。

方法

Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。

感觉方法有点像面向对象里面类的成员函数。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。

定义方法的一般格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

函数和方法的区别

函数将变量作为参数:Function1(recv)

方法在变量上被调用:recv.Method1()

在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。

接收者必须有一个显式的名字,这个名字必须在方法中被使用。

在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法间的关联由接收者来建立。方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。

鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。

接口(Interfaces)与反射(reflection)

接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

通过如下格式定义接口:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。实现某个接口的类型(除了实现接口方法外)可以有其他的方法。一个类型可以实现多个接口。接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。

Goroutine和channel

协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。任何 Go 程序都必须有的 main() 函数也可以看做是一个协程,尽管它并没有通过 go 来启动。协程可以在程序初始化的过程中运行(在 init() 函数中)。在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用 runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched() 可以使计算均匀分布,使通信不至于迟迟得不到响应。

 

赞(2) 打赏
转载请注明出处:LinMao's Blog(林茂的博客) » GO语言学习笔记

评论 抢沙发

静态归档版本,评论功能已关闭。
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

LinMao's Blog(林茂的博客)

了解更多联系我们

觉得文章有用就打赏一下作者吧~

支付宝扫一扫打赏

支付宝

微信扫一扫打赏

微信