Golang简单入门笔记

基础

关键字

关键字有25个。关键字不能用于自定义名字,只能在特定语法结构中使用。

1
2
3
4
5
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

预定义名字

大约有30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

1
2
3
4
5
6
7
8
9
10
内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数: make len cap new append copy close delete
complex real imag
panic recover

其中:

  • byte 是 uint8 类型的别名,存储 raw data
  • rune 是 int32 类型的别名,存储一个 Unicode code point 字符

这些内部预先定义的名字并不是关键字,你可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱。

命名规则

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。

如果一个名字是在函数内部定义,那么它的就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问。

命名的建议:

  • 推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。
  • 包本身的名字一般总是用小写字母。
  • 名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样。
  • 如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

声明和变量

  • var 用于声明变量
  • const 用于声明常量
  • type 用于声明一个新的类型
  • func 用于声明一个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo int // 无初值的声明
var foo int = 42 // 带初值的声明
var foo, bar int = 42, 1302 // 一次性声明并初始化多个变量
var foo = 42 // 类型推断,由使用的上下文决定
foo := 42 // 简短模式,自动推断类型,并且必须在函数体内部
const constant = "This is a constant" //声明一个常量
type MyInt struct{ //声明一个类型
}
func fooFuc(){ //声明一个函数
}
  • var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。
  • 简短模式有以下限制:
    • 定义变量,同时显式初始化。
    • 不能提供数据类型。
    • 只能用在函数内部。
    • 由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。
    • 在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错。
  • 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的。

匿名变量

匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。

1
2
3
4
5
6
7
8
func GetData() (int, int) {
return 100, 200
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}

变量的生命周期

  • 对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。
  • 局部变量的声明周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。
  • 函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。

常量

常量表达式的值在编译期计算,而不是在运行期。

iota 常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)

new 和 make

new 和 make 是两个内置函数,主要用来创建并分配类型的内存。new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化。

  • new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。
1
2
3
4
5
6
7
8
9
10
11
12
13
var sum *int
sum = new(int) //分配空间
*sum = 98
fmt.Println(*sum)
type Student struct {
name string
age int
}
var s *Student
s = new(Student) //分配空间。如果我们不使用 new 函数为自定义类型分配空间,就会报错
s.name ="dequan"
fmt.Println(s)
  • make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

运算符

运算符 描述
+
-
*
/
% 取余
& 按位与
` ` 按位或
^ 按位异或
&^ 按位清除(AND NOT)&^AND NOT(x, y) = AND(x, NOT(Y))
<< 左移
>> 右移
== 相等
!= 不等
< 小于
<= 小于等于
> 大于
>= 大于等于
&& 逻辑与
` ` 逻辑或
! 取反
& 寻址(生成指针)
* 获取指针指向的数据
<- 向 channel 中发送 / 接收数据

包( package )

  • main package 才是可执行文件
  • package 名字与 import 路径的最后一个单词一致(如导入 math/rand 则 package 叫 rand)
  • 写开头的标识符(变量名、函数名…),对其他 package 是可访问的
  • 小写开头的标识符,对其他 package 是不可见的

包的初始化

  • 包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。
  • 如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
  • 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。
  • 初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。

init初始化函数

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package popcount
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}

数据类型

数据类型分为:基础类型、复合类型、引用类型和接口类型。

  • 基础类型,包括:数字、字符串和布尔型等
  • 复合数据类型:数组和结构体等
  • 引用类型包括:指针、切片、字典、函数、通道等

基础类型

  • uint 32 位或 64 位
  • uint8 无符号 8 位整型 (0 到 255)
  • uint16 无符号 16 位整型 (0 到 65535)
  • uint32 无符号 32 位整型 (0 到 4294967295)
  • uint64 无符号 64 位整型 (0 到 18446744073709551615)
  • int 32 位或 64 位
  • int8 有符号 8 位整型 (-128 到 127)
  • int16 有符号 16 位整型 (-32768 到 32767)
  • int32 有符号 32 位整型 (-2147483648 到 2147483647)
  • int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
  • byte uint8 的别名(type byte = uint8)
  • rune int32 的别名(type rune = int32),表示一个 unicode 码
  • uintptr “无符号整型,用于存放一个指针是一种无符号的整数类型,没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程是才需要,特别是Go 语言和 C 语言函数库或操作系统接口相交互的地方。”
  • float32 IEEE-754 32 位浮点型数
  • float64 IEEE-754 64 位浮点型数
  • complex64 32 位实数和虚数
  • complex128 64 位实数和虚数
  • string 字符串,默认值是空字符串,而非 NULL

复合数据类型

复合数据类型类型包括数组、结构体、切片、字典等。

struct

Go 语言中没有 class 类的概念,取而代之的是 struct,struct 的方法对应到类的成员函数。

结构类型可以用来描述一组数据值,这组值的本质既可以是原始的,也可以是非原始的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// struct 是一种类型,也是字段成员的集合体
// 声明 struct
type Vertex struct {
X, Y int
}
// 结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段。
// 初始化 struct
var v = Vertex{1, 2} // 字段名有序对应值
var v = Vertex{X: 1, Y: 2} // 字段名对应值
var v = []Vertex{{1,2},{5,2},{5,5}} // 初始化多个 struct 组成的 slice
//用键值对填充结构体的例子
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
// 创建指针类型的结构体。实例化以后,实例的类型为 *T,属于指针。
vertex := new(Vertex)
// 取结构体的地址实例化。对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作。
// 实例化以后,实例的类型为 *T,属于指针。
v := &Vertex{}
// 访问成员
v.X = 4
// 在 func 关键字和函数名之间,声明接收者是 struct
// 在方法内部,struct 实例被复制,传值引用
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// 调用方法(有接收者的函数)
v.Abs()
// 有的方法接收者是指向 struct 的指针
// 此时在方法内调用实例,将是传址引用
func (v *Vertex) add(n float64) {
v.X += n
v.Y += n
}
//取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
func newCommand(name string, varref *int, comment string) *Command {
return &Command{
Name: name,
Var: varref,
Comment: comment,
}
}
cmd = newCommand(
"version",
&version,
"show version",
)

匿名结构体

使用 map[string]interface{} 开销更小且更为安全。

1
2
3
point := struct {
X, Y int
}{1, 2}

array

1
2
3
4
5
6
7
8
9
10
11
12
13
var a [10]int // 声明长度为 10 的 int 型数组,注意数组类型 = (元素类型 int,元素个数 10)
a[3] = 42 // 设置元素值
i := a[3] // 读取元素值
// 声明并初始化数组
var a = [2]int{1, 2}
a := [2]int{1, 2} // 简短声明
a := [...]int{1, 2} // 数组长度使用 ... 代替,编译器会自动计算元素个数
//遍历数组
for k, v := range a {
fmt.Println(k, v)
}

list

在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。

初始化:

  • 变量名 := list.New()
  • var 变量名 list.List
1
2
3
4
5
6
7
8
9
10
11
l := list.New()
l.PushBack("fist") //在尾部插入
l.PushFront(67) //在头部插入
l.Remove(element) // 移除 element 变量对应的元素。
//遍历
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}

slice

切片是有三个字段的数据结构,指向底层数组的指针,切片访问元素的个数(长度),切片允许增长到的元素的个数(容量),这种数据结构便于使用和管理数据集。切片围绕动态数组的概念构建的,可以按需自动增长和缩小。切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法,切片的底层内存是在连续块中分配的。

语法:

slice [开始位置 : 结束位置]

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var a []int // 声明 slice,相当于声明未指定长度的数组
var a = []int {1, 2, 3, 4} // 声明并初始化 slice (基于 {} 中给出的底层数组)
a := []int{1, 2, 3, 4} // 简短声明
chars := []string{0:"a", 2:"c", 1: "b"} // ["a", "b", "c"]
var b = a[lo:hi] // 创建从 lo 到 hi-1 的 slice
var b = a[1:4] // 创建从 1 到 3 的 slice
var b = a[:3] // 缺省 start index 则默认为 0
var b = a[3:] // 缺省 end index 则默认为 len(a)
a = append(a,17,3) // 向 slice a 中追加 17 和 3
c := append(a,b...) // 合并两个 slice
// 使用 make 创建 slice
a = make([]byte, 5, 5) // 第一个参数是长度,第二个参数是容量
a = make([]byte, 5) // 容量参数是可选的
// 从数组创建 slice
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // slice s 指向底层数组 x
// 内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

map

map (映射)是一种数据结构,用于存储一系列无序的键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var m map[string]int
m = make(map[string]int)
m["key"] = 42
fmt.Println(m["key"])
delete(m, "key")
elem, ok := m["key"] // 检查 m 中是否键为 key 的元素,如果有 ok 才为 true
// 使用键值对的形式来初始化 map
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
// 从映射获取值并判断键是否存在
value, exits := colors["blue"]
if exits {
// blue
fmt.Println(value)
}
// 使用range迭代映射
for key, value := range colors {
// key:red value:red
fmt.Printf("key:%s value:%s\n", key, value)
}

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。1.9 版本中提供了一种效率较高的并发安全的 sync.Map。

指针

一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加 &操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

ptr := &v // v 的类型为 T

==其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。==

变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。

从指针获取指针指向的值: 当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值。

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。

*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

通道

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。

通道的声明如下:

1
ch := make(chan 类型)

如: ch := make(chan int) // ch has type 'chan int'

两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。

channel的发送和接受操作:

  • 一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
  • 发送和接收两个操作都使用<-运算符
  • 在发送语句中,<-运算符分割channel和要发送的值
  • 在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的

示例如下:

1
2
3
ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded

不带缓存的channel

  • 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作。
  • 如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
  • 无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前。

串联的Channels(Pipeline)

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}

单方向的Channel

Go语言的类型系统提供了单方向的channel类型,分别用于只发送或只接收的channel。箭头<-和关键字chan的相对位置表明了channel的方向。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}

带缓存的Channels

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

数据类型转换

由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:

1
2
3
xvalueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)

只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)。

函数

函数的基本语法为:

1
2
3
func 函数名(形式参数列表)(返回值列表){
函数体
}

基本函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 最简单的函数
func functionName() {}
// 带参数的函数(注意类型也是放在标识符之后的)
func functionName(param1 string, param2 int) {}
// 类型相同的多个参数
func functionName(param1, param2 int) {}
// 声明返回值的类型
func functionName() int {
return 42
}
// 定义多个变量接受,多返回值
func div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// 一次返回多个值
func returnMulti() (int, string) {
return 42, "foobar"
}
var x, str = returnMulti()
// 可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回
func returnMulti2() (n int, s string) {
n = 42
s = "foobar"
// n 和 s 会被返回
return
}
var x, str = returnMulti2()
// 函数作为第一类型,可作为参数或返回值
func test(x int) func() {
// 匿名函数
return func() {
fmt.Println(x)
}
}

匿名函数(函数作为值和回调使用)

匿名函数语法为:

1
2
3
func(参数列表)(返回参数列表){
函数体
}

一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 在定义时调用匿名函数
func(data int) {
fmt.Println("hello", data)
}(100)
func main() {
// 将函数作为值,赋给变量
add := func(a, b int) int {
return a + b
}
// 使用变量直接调用函数
fmt.Println(add(3, 4))
}
// 使用匿名函数实现操作封装
var skillParam = flag.String("skill", "", "skill to perform")
func main() {
flag.Parse()
var skill = map[string]func(){
"fire": func() {
fmt.Println("chicken fire")
},
"run": func() {
fmt.Println("soldier run")
},
"fly": func() {
fmt.Println("angel fly")
},
}
if f, ok := skill[*skillParam]; ok {
f()
} else {
fmt.Println("skill not found")
}
}
// 回调函数作用域:在定义回调函数时能访问外部函数的值
func scope() func() int{
outer_var := 2
foo := func() int { return outer_var}
return foo
}
func another_scope() func() int{
// 编译错误,两个变量不在此函数作用域内
// undefined: outer_var
outer_var = 444
return foo
}
// 回调函数不会修改外部作用域的数据
func outer() (func() int, int) {
outer_var := 2
inner := func() int {
outer_var += 99 // 试着使用外部作用域的 outer_var 变量
return outer_var // 返回值是 101,但只在 inner() 内部有效
}
return inner, outer_var // 返回值是 inner, 2 (outer_var 仍是 2)
}
inner, outer_var := outer(); // inner, 2
inner(); // 返回 101
inner(); // 返回 200 // 回调函数的特性

闭包

函数 + 引用环境 = 闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
)
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
func main() {
// 创建一个玩家生成器
generator := playerGen("high noon")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
}

可变参数函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
fmt.Println(adder(1, 2, 3)) // 6
fmt.Println(adder(9, 9)) // 18
nums := []int{10, 20, 30}
fmt.Println(adder(nums...)) // 60
}
// 在函数的最后一个参数类型前,使用 ... 可表明函数还能接收 0 到多个此种类型的参数
// 下边的函数在调用时传多少个参数都可以
func adder(args ...int) int {
total := 0
for _, v := range args { // 使用迭代器逐个访问参数
total += v
}
return total
}

range 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 迭代数组或 slice
for i, e := range a {
// i 是索引
// e 是元素值
}
// 如果你只要值,可用 _ 来丢弃返回的索引
for _, e := range a {
}
// 如果你只要索引
for i := range a {
}
// 在 Go 1.4 以前的版本,如果 i 和 e 你都不用,直接 range 编译器会报错
for range time.Tick(time.Second) {
// 每隔 1s 执行一次
}

流程控制

if…else…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//一般的条件判断
x := 1
if x > 0 {
return x
} else {
return -x
}
// 在条件判断语句前可以有一条语句
if a := b + c; a < 42 {
return a
} else {
return a - 42
}

switch…case…

case 语句自带 break,想执行所有 case 需要手动 fallthrough

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// switch 分支语句
switch arg {
case "string":
fmt.Println("string")
case 1
fmt.Println("number")
case arg < 1:
fmt.Println("运算")
case case ' ', '?', '&', '=', '#', '+', '%': // 多个 case 可使用逗号分隔统一处理
fmt.Println("Should escape")
default:
fmt.Println("Other")
}
// 可在判断变量之前加入一条赋值语句
switch os := runtime.GOOS; os {
case "darwin": ...
}

for

Go 语言中循环结构只有 for,没有 do、while、foreach 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for i := 1; i < 10; i++ {
}
// 等效于 while 循环
for ; i < 10; {
}
// 只有一个判断条件时可省去分号
for i < 10 {
}
// 无条件循环时,等效于 while(true)
for {
}

错误处理

处理运行时错误

Go 中没有异常处理机制,函数在调用时在有可能会产生错误,可返回一个 Error 类型的值,Error 接口:

1
2
3
type error interface {
Error() string
}

自定义一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var err = errors.New("this is an error")
// 创建错误对象
func New(text string) error {
return &errorString{text}
}
// 错误字符串
type errorString struct {
s string
}
// 返回发生何种错误
func (e *errorString) Error() string {
return e.s
}

一个可能产生错误的函数:

1
2
3
4
5
6
7
8
9
10
func doStuff() (int, error) {
}
func main() {
result, err := doStuff()
if err != nil {
// 错误处理
}
// 使用 result 处理正常逻辑
}

defer(延迟错误处理)

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

关键字 defer 的用法类似于面向对象编程语言 Java 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。因为 defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。

宕机(panic)与宕机恢复(recover)

Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于 panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。

手动触发宕机: panic("crash")

Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

panic 和 recover 的关系

panic 和 recover 的组合有如下特性:

  • 有 panic 没 recover,程序宕机。
  • 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。

提示

虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。

在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。

如果想在捕获错误时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。

OOP

对象

struct

继承

Go语言中的继承是通过内嵌或组合来实现的

  • 1) 内嵌的结构体可以直接访问其成员变量
  • 2) 内嵌结构体的字段名是它的类型名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人类
type Human struct {
Walkable // 人类能行走
}
// 鸟类
type Bird struct {
Walkable // 鸟类能行走
Flying // 鸟类能飞行
}
func main() {
// 实例化鸟类
b := new(Bird)
fmt.Println("Bird: ")
b.Fly()
b.Walk()
// 实例化人类
h := new(Human)
fmt.Println("Human: ")
h.Walk()
}

构造方法

Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//多种方式创建和初始化结构体——模拟构造函数重载
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{ //定义用名字构造猫结构的函数,返回 Cat 指针。
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{ //定义用颜色构造猫结构的函数,返回 Cat 指针。
Color: color,
}
}
//带有父子关系的结构体的构造和初始化——模拟父级构造调用
type BlackCat struct {
Cat // 嵌入Cat, 类似于继承
}
// “构造基类”
func NewCat(name string) *Cat {
return &Cat{
Name: name,
}
}
// “构造子类”
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{}
cat.Color = color
return cat
}

方法

Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。

接收器语法格式:

1
2
3
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}

对各部分的说明:

  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:格式与函数定义一致。

接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

指针接收器

指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
// 定义属性结构
type Property struct {
value int // 属性值
}
// 设置属性值
func (p *Property) SetValue(v int) {
// 修改p的成员变量
p.value = v
}
// 取属性值
func (p *Property) Value() int {
return p.value
}
func main() {
// 实例化属性
p := new(Property)
// 设置值
p.SetValue(100)
// 打印值
fmt.Println(p.Value())
}

非指针接收器

当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
// 定义点结构
type Point struct {
X int
Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
// 成员值与参数相加后返回新的结构
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
// 初始化点
p1 := Point{1, 1}
p2 := Point{2, 2}
// 与另外一个点相加
result := p1.Add(p2)
// 输出结果
fmt.Println(result)
}

指针和非指针接收器的使用

  • 在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

  • 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。

  • 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。熟悉C或者C艹的人这里应该很快能明白。

接口

接口是用来定义行为的类型,这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。Go 接口实现机制很简洁,只要目标类型方法包含接口声明的全部方法,就被视为实现了该接口,无需做显示声明。当然,目标类可以实现多个接口。如果接口没有任何方法声明,那么就是一个空接口(interface{}),它的用途类似 Object,可被赋值为任何类型的对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 声明接口
type Awesomizer interface {
Awesomize() string
}
// 无需手动声明 implement 接口
type Foo struct {}
// 自定义类型如果实现了接口的所有方法,那它就自动实现了该接口
func (foo Foo) Awesomize() string {
return "Awesome!"
}

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。

并发

goroutine

goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

使用普通函数创建 goroutine:

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。语法:go 函数名( 参数列表 )。使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。

也可以使用匿名函数创建:

1
2
3
go func( 参数列表 ){
函数体
}( 调用参数列表 )

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"time"
)
func running() {
var times int
// 构建一个无限循环
for {
times++
fmt.Println("tick", times)
// 延时1秒
time.Sleep(time.Second)
}
}
func main() {
// 并发执行程序
go running()
// 接受命令行输入, 不做任何事情
var input string
fmt.Scanln(&input)
}

反射

Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。

反射是由 reflect 包提供的。 它定义了两个重要的类型, Type 和 Value。

  • 一个 Type 表示一个Go类型。函数 reflect.TypeOf 接受任意的 interface{} 类型, 并以reflect.Type形式返回其动态类型
  • 一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型, 并返回一个装载着其动态值的 reflect.Value。