GO语言基础

一、绪论

1 Go语言介绍

Go 即Golang,是Google公司2009年11月正式对外公开的一门编程语言。

Go是静态强类型语言,是区别于解析型语言的编译型语言(静态:类型固定 强类型:不同类型不允许直接运算)。

解析型语言——源代码是先翻译为中间代码,然后由解析器对代码进行解释执行。

编译型语言——源代码编译生成机器语言,然后由机器直接执行机器码即可执行。

2 Go语言特性

  • 跨平台的编译型语言

  • 交叉编译(在win平台可编译出mac平台的可执行文件)

  • 语法接近C语言

  • 管道(channel),切片(slice),并发(routine)

  • 有垃圾回收的机制

  • 支持面向对象和面向过程的编程模式

3 go适合做什么

  • 服务端开发
  • 分布式系统,微服务
  • 网络编程
  • 区块链开发
  • 内存KV数据库,例如boltDB、levelDB
  • 云平台

4 下载和安装

开发环境:

IDE:

  • GoLand

  • vscode

5 配置GOPATH和GOROOT

  • GOPATH:代码存放路径,该目录下有三个文件夹(如果没有,要手动创建),
    windows和mac默认在用户名下的go文件夹(GOPATH=“/Users/用户名/go”),所有代码必须写在这个路径下的src文件夹下,否则无法编译,可以手动修改。
1
2
3
-src——源码(包含第三方的和自己项目的)
-bin——编译生成的可执行程序
-pkg——编译时生成的对象文件
  • GOROOT:go开发工具包的安装路径,默认:C:\go

    将GOROOT下的bin路径加入环境变量(默认已处理),这样任意位置敲 go 都能找到该命令

6 命令介绍

直接在终端中输入 go help 即可显示所有的 go 命令以及相应命令功能简介,主要有下面这些:

  • build: 编译包和依赖
  • clean: 移除对象文件(go clean :删除编译的可执行文件)
  • doc: 显示包或者符号的文档
  • env: 打印go的环境信息
  • bug: 启动错误报告
  • fix: 运行go tool fix
  • fmt: 运行gofmt进行格式化(go fmt :自动将代码格式)
  • generate: 从processing source生成go文件
  • get: 下载并安装包和依赖(go get github.com/astaxie/beego:下载beego框架)
  • install: 编译并安装包和依赖(go install 项目名:会将go编译,并放到bin路径下)
  • list: 列出包
  • run: 编译并运行go程序
  • test: 运行测试
  • tool: 运行go提供的工具
  • version: 显示go的版本
  • vet: 运行go tool vet

6.1 build 和 run 命令

就像其他静态类型语言一样,要执行 go 程序,需要先编译,然后在执行产生的可执行文件。go build 命令就是用来编译 go程序生成可执行文件的。但并不是所有的 go 程序都可以编译生成可执行文件的, 要生成可执行文件,go程序需要满足两个条件:

  • 该go程序需要属于main包
  • 在main包中必须还得包含main函数

也就是说go程序的入口就是 main.main, 即main包下的main函数, 例子(test.go):

编译hello.go,然后运行可执行程序:

1
2
3
$ go build test.go   # 将会生成可执行文件 test
$ ./test # 运行可执行文件
Hello, World!

上面就是 go build 的基本用法,另外如果使用 go build 编译的不是一个可执行程序,而是一个包,那么将不会生成可执行文件。

go run 命令可以将上面两步并为一步执行(不会产生中间文件)。

1
2
$ go run test.go
Hello, World!

上面两个命令都是在开发中非常常用的。

此外 go clean 命令,可以用于将清除产生的可执行程序:

1
2
$ go clean    # 不加参数,可以删除当前目录下的所有可执行文件
$ go clean hello.go # 会删除对应的可执行文件

6.2 get 命令

这个命令同样也是很常用的,我们可以使用它来下载并安装第三方包, 使用方式:

1
go get src

从指定源上面下载或者更新指定的代码和依赖,并对他们进行编译和安装,例如我们想使用 beego 来开发web应用,我们首先就需要获取 beego:

1
go get github.com/astaxie/beego

这条命令将会自动下载安装 beego 以及它的依赖,然后我们就可以使用下面的方式使用:

1
2
3
4
5
6
7
package main

import "github.com/astaxie/beego" # 这里需要使用 src 下的完整路径

func main() {
beego.Run()
}

7 第一个go程序

1
2
3
4
5
6
7
8
9
10
11
12
13
![第一个go程序](../../../../桌面备份/note/第一个go程序.jpg)// go语言的注释

/*多行

注释*/

package main // 声明包名是main,每一个go文件都必须属于某一个包

import "fmt" // 导入内置包

func main() { // func函数关键字,定义main函数 类似c语言,编译型语言需要有入口函数: main包下的main函数
fmt.Println("hello,world!") // 打印函数
}

bulid命令编译,再执行exe:

或者使用run命令,编译并执行:

1
go run 1.go

8 命名规范

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个简单的命名规则:

1 一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线

2 大写字母和小写字母是不同的:Name和name是两个不同的变量

3 关键字和保留字都不建议用作变量名

Go语言中关键字有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

go语言中有37个保留字,主要对应内建的常量、类型和函数

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

注意:

  1. 这些保留字并不是关键字,可以在定义中重新使用它们。在一些特殊的场景中重新定义它们也是有意义的,但是也要注意避免过度而引起语义混乱
  2. 如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母
  3. 名字的长度没有逻辑限制,但是Go语言的风格是尽量使用短小的名字,对于局部变量尤其是这样;你会经常看到i之类的短名字,而不是冗长的theLoopIndex命名。通常来说,如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义
  4. 在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。因此,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名,但是一般不会用quote_rune_to_ASCII和parse_request_line这样的命名。而像ASCII和HTML这样的缩略词则避免使用大小写混合的写法,它们可能被称为htmlEscape、HTMLEscape或escapeHTML,但不会是escapeHtml
  5. go文件的名字,建议用下划线的方式命名(参见go源码)

二、变量和常量

1 定义单个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main(){
// 定义变量的第一种方法:全定义 var name type/var关键字 变量名 变量类型
// var name type = initialvalue 该语法可以声明并初始化
var age int = 10 // 变量如果声明就必须在后面使用,否则会报错
fmt.Println(age)

// 定义变量的第二种方法:类型推导 var name = initialvalue var关键字 变量名 = 值
// Go 能够自动推断具有初始值的变量的类型
var a = 50
fmt.Println(a)
fmt.Printf("%T", a) // %T 打印变量a的类型

// 定义变量的第三种方法:简略声明 var和类型都不写
// 简短声明要求 := 操作符左边的所有变量都有初始值
b:=20
fmt.Println(b)
}

2 定义多个变量

Go 能够通过一条语句声明多个变量。

1
2
3
// 定义多个变量var name1, name2 type = initialvalue1, initialvalue
var s1,s2 int = 10,20
fmt.Println(s1,s2)

在有些情况下,我们可能会想要在一个语句中声明不同类型的变量。其语法如下:

1
2
3
4
var (  
name1 = initialvalue1,
name2 = initialvalue2
)

使用上述语法,下面的程序声明不同类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
var (
name = "naveen"
age = 29
height int
)
fmt.Println("my name is", name, ", age is", age, "and height is", height)
}

3 注意

简短声明的语法要求 := 操作符的左边至少有一个变量是尚未声明的。考虑下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
a, b := 20, 30 // 声明变量a和b
fmt.Println("a is", a, "b is", b)
b, c := 40, 50 // b已经声明,但c尚未声明
fmt.Println("b is", b, "c is", c)
b, c = 80, 90 // 给已经声明的变量b和c赋新值
fmt.Println("changed b is", b, "c is", c)
}

在上面程序中的第 8 行,由于 b 已经被声明,而 c 尚未声明,因此运行成功

但是如果我们运行下面的程序

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
a, b := 20, 30 // 声明a和b
fmt.Println("a is", a, "b is", b)
a, b := 40, 50 // 错误,没有尚未声明的变量
}

变量也可以在运行时进行赋值。考虑下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"math"
)

func main() {
a, b := 145.8, 543.8
c := math.Min(a, b)
fmt.Println("minimum value is ", c)
}

在上面的程序中,c 的值是运行过程中计算得到的,即 a 和 b 的最小值

4 常量

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:

1
const identifier [type] = value

你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

多个相同类型的声明可以简写为:

1
const c_name1, c_name2 = value1, value2

顾名思义,常量不能再重新赋值为其他的值。因此下面的程序将不能正常工作,它将出现一个编译错误: cannot assign to a.

1
2
3
4
5
6
package main

func main() {
const a = 55 // 允许
a = 89 // 不允许重新赋值
}

常量的值会在编译的时候确定。因为函数调用发生在运行时,所以不能将函数的返回值赋值给常量。

5 iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量。

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

iota 可以被用作枚举值:

1
2
3
4
5
const (
a = iota
b = iota
c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
const (
a = iota
b
c
)

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

以上实例运行结果为:

1
0 1 2 ha ha 100 100 7 8

三、运算符

1 算数运算符

下表列出了所有Go语言的算术运算符。假定 A 值为 10,B 值为 20。

运算符 描述 实例
+ 相加 A + B 输出结果 30
- 相减 A - B 输出结果 -10
* 相乘 A * B 输出结果 200
/ 相除 B / A 输出结果 2
% 求余 B % A 输出结果 0
++ 自增 A++ 输出结果 11
自减 A-- 输出结果 9

2 关系运算符

下表列出了所有Go语言的关系运算符。假定 A 值为 10,B 值为 20。

运算符 描述 实例
== 检查两个值是否相等,如果相等返回 True 否则返回 False。 (A == B) 为 False
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 (A != B) 为 True
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 (A > B) 为 False
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 (A < B) 为 True
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 (A >= B) 为 False
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 (A <= B) 为 True

3 逻辑运算符

下表列出了所有Go语言的逻辑运算符。假定 A 值为 True,B 值为 False。

运算符 描述 实例
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 (A && B) 为 False
|| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 (A || B) 为 True
! 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 !(A && B) 为 True

4 位运算符

位运算符对整数在内存中的二进制位进行操作。

下表列出了位运算符 &, |, 和 ^ 的计算:

p q p & q p | q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

Go 语言支持的位运算符如下表所示。假定 A 为60,B 为13:

运算符 描述 实例
& 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 (A & B) 结果为 12, 二进制为 0000 1100
| 按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或 (A | B) 结果为 61, 二进制为 0011 1101
^ 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (A ^ B) 结果为 49, 二进制为 0011 0001
<< 左移运算符"<<“是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<“左边的运算数的各二进位全部左移若干位,由”<<"右边的数指定移动的位数,高位丢弃,低位补0。 A << 2 结果为 240 ,二进制为 1111 0000
>> 右移运算符">>“是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>“左边的运算数的各二进位全部右移若干位,”>>"右边的数指定移动的位数。 A >> 2 结果为 15 ,二进制为 0000 1111

5 赋值运算符

下表列出了所有Go语言的赋值运算符。

运算符 描述 实例
= 简单的赋值运算符,将一个表达式的值赋给一个左值 C = A + B 将 A + B 表达式结果赋值给 C
+= 相加后再赋值 C += A 等于 C = C + A
-= 相减后再赋值 C -= A 等于 C = C - A
*= 相乘后再赋值 C *= A 等于 C = C * A
/= 相除后再赋值 C /= A 等于 C = C / A
%= 求余后再赋值 C %= A 等于 C = C % A
<<= 左移后赋值 C <<= 2 等于 C = C << 2
>>= 右移后赋值 C >>= 2 等于 C = C >> 2
&= 按位与后赋值 C &= 2 等于 C = C & 2
^= 按位异或后赋值 C ^= 2 等于 C = C ^ 2
|= 按位或后赋值 C |= 2 等于 C = C | 2

6 其它运算符

下表列出了Go语言的其他运算符。

运算符 描述 实例
& 返回变量存储地址 &a; 将给出变量的实际地址。
* 指针变量。 *a; 是一个指针变量

7 运算符优先级

有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:

优先级 运算符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

当然,你可以通过使用括号来临时提升某个表达式的整体运算优先级。

四、数据类型

下面是 Go 支持的基本类型:

  • bool 布尔型
  • 数字类型
    • int8, int16, int32, int64, int 整型
    • uint8, uint16, uint32, uint64, uint 无符号整型
    • float32, float64 浮点型
    • complex64, complex128 复数
    • byte 类似 uint8
    • rune 类似 int32
  • string 字符串类型

1 bool

bool 类型表示一个布尔值,值为 true 或者 false

2 有符号整型

int8:表示 8 位有符号整型
大小:8 位
范围:-128~127

int16:表示 16 位有符号整型
大小:16 位
范围:-32768~32767

int32:表示 32 位有符号整型
大小:32 位
范围:-2147483648~2147483647

int64:表示 64 位有符号整型
大小:64 位
范围:-9223372036854775808~9223372036854775807

int:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型。
大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。
范围:在 32 位系统下是 -2147483648~2147483647,而在 64 位系统是 -9223372036854775808~9223372036854775807。

3 无符号整型

uint8:表示 8 位无符号整型
大小:8 位
范围:0~255

uint16:表示 16 位无符号整型
大小:16 位
范围:0~65535

uint32:表示 32 位无符号整型
大小:32 位
范围:0~4294967295

uint64:表示 64 位无符号整型
大小:64 位
范围:0~18446744073709551615

uint:根据不同的底层平台,表示 32 或 64 位无符号整型。
大小:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。
范围:在 32 位系统下是 0~4294967295,而在 64 位系统是 0~18446744073709551615。

4 浮点型

float32:32 位浮点数
float64:64 位浮点数

5 复数类型

complex64:实部和虚部都是 float32 类型的的复数。
complex128:实部和虚部都是 float64 类型的的复数。

内建函数 complex用于创建一个包含实部和虚部的复数。complex 函数的定义如下:

1
func complex(r, i FloatType) ComplexType

该函数的参数分别是实部和虚部,并返回一个复数类型。实部和虚部应该是相同类型,也就是 float32 或 float64。如果实部和虚部都是 float32 类型,则函数会返回一个 complex64 类型的复数。如果实部和虚部都是 float64 类型,则函数会返回一个 complex128 类型的复数。

还可以使用简短语法来创建复数:

1
c := 6 + 7i

6 string 类型

在 Golang 中,字符串是字节的集合。如果你现在还不理解这个定义,也没有关系。我们可以暂且认为一个字符串就是由很多字符组成的。我们后面会在一个教程中深入学习字符串。 下面编写一个使用字符串的程序。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
first := "Naveen"
last := "Ramanathan"
name := first +" "+ last
fmt.Println("My name is",name)
}

上面程序中,first 赋值为字符串 “Naveen”,last 赋值为字符串 “Ramanathan”。+ 操作符可以用于拼接字符串。我们拼接了 first、空格

和 last,并将其赋值给 name。上述程序将打印输出 My name is Naveen Ramanathan

7 类型转换

Go 有着非常严格的强类型特征。Go 没有自动类型提升或类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
i := 55 //int
j := 67.8 //float64
sum := i + j //不允许 int + float64
fmt.Println(sum)
}

上面的代码在 C 语言中是完全合法的,然而在 Go 中,却是行不通的。i 的类型是 int ,而 j 的类型是 float64 ,我们正试图把两个不同类型的数相加,Go 不允许这样的操作。如果运行程序,你会得到 main.go:10: invalid operation: i + j (mismatched types int and float64)

要修复这个错误,i 和 j 应该是相同的类型。在这里,我们把 j 转换为 int 类型。把 v 转换为 T 类型的语法是 T(v)。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
i := 55 //int
j := 67.8 //float64
sum := i + int(j) //j is converted to int 舍弃小数点后的数字
fmt.Println(sum)
}

现在,当你运行上面的程序时,会看见输出 122

赋值的情况也是如此。把一个变量赋值给另一个不同类型的变量,需要显式的类型转换。下面程序说明了这一点。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)

func main() {
i := 10
var j float64 = float64(i) // 若没有显式转换,该语句会报错
fmt.Println("j", j)
}

在第 9 行,i 转换为 float64 类型,接下来赋值给 j。如果不进行类型转换,当你试图把 i 赋值给 j 时,编译器会抛出错误。

8 数据类型的默认值

如果只定义不赋值,则:

  • 数字类型是0
  • 字符串类型是空字符串
  • 布尔类型是false
1
2
3
4
5
6
func main() {
var a int
var b string
var c bool
fmt.Println(a,b,c)
}

五、条件语句

1 if…else语句

1
2
3
4
5
6
7
8
9
func main() {
var a int = 10
if a<20 {
fmt.Println("如果条件为true则执行if内部")
}else {
fmt.Println("如果条件为false则执行else内部")
}

}

2 switch语句

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

switch 语句执行的过程从上至下,直到找到匹配项,与c语言不同,匹配项后面也不需要再加 break。

switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用

fallthrough

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
func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90

switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}

switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
}

六、循环语句

Go没有while循环语句,只有for循环。

for 循环是一个循环控制结构,可以执行指定次数的循环。

Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。

和 C 语言的 for 一样:

1
for init; condition; post { }

和 C 的 while 一样:

1
for condition { }

和 C 的 for(;;) 一样:

1
for { }
  • init: 一般为赋值表达式,给控制变量赋初值;
  • condition: 关系表达式或逻辑表达式,循环控制条件;
  • post: 一般为赋值表达式,给控制变量增量或减量。

for 循环的range格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

1
2
3
for key, value := range oldMap {
newMap[key] = value
}

计算 1 到 10 的数字之和:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
fmt.Println(sum)
}

init 和 post 参数是可选的,我们可以直接省略它,类似 While 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
sum := 1
for ; sum <= 10; {
sum += sum
}
fmt.Println(sum)

// 这样写也可以,更像 While 语句形式
for sum <= 10{
sum += sum
}
fmt.Println(sum)
}

无限循环

1
2
3
4
5
6
7
8
func main() {
sum := 0
for {
sum++ // 无限循环下去
}
fmt.Println(sum) // 无法输出
}
要停止无限循环,可以在命令窗口按下ctrl-c 。
  • 关键字break

    break 语句用于在完成正常执行之前突然终止 for 循环,之后程序将会在 for 循环下一行代码开始执行。

  • 关键字continue

    continue 语句用来跳出 for 循环中当前循环。在 continue 语句后的所有的 for 循环语句都不会在本次循环中执行。循环体会在一下次循环中继续执行。

七、函数

函数是基本的代码块,用于执行一个任务。

Go 语言至少有一个入口main() 函数。

1 函数定义

函数定义的格式如下:

1
2
3
func function_name( [parameter list] ) [return_types] {
函数体
} // 函数的作用范围就在大括号之间{}

函数定义解析:

  • func:函数由 func 开始声明
  • function_name:函数名称,参数列表和返回值类型构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。声明一个参数的语法采用 参数名 参数类型 的方式,多个参数的声明用逗号隔开。
  • return_types:返回类型,规定了函数的返回值类型return_types。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个函数
func max(a,b int) int { //如果有连续若干个参数,它们的类型一致,那么我们无须一一罗列,只需在最后一个参数后添加该类型。
ret := a
if a>b{
fmt.Println("a比b大")
}else if a==b {
fmt.Println("a等于b")
}else {
fmt.Println("b比a大")
ret = b
}
return ret
}

2 函数调用

调用函数,(如果需要参数)向函数传递参数,(如果有返回值)并得到返回值,例如:

1
2
3
4
func main() {
ret := max(10,20) // 调用函数,得到返回值
fmt.Println(ret)
}

得到输出:b比a大\n20

3 其它

调用函数,可以通过两种方式来传递参数:值传递和引用传递。

默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

Go 语言支持一个函数可以有多个返回值。如果一个函数有多个返回值,那么这些返回值必须用括号括起来。

1
2
3
4
5
6
7
8
func swap(x, y string) (string, string) { //这些返回值必须用括号括起来
return y, x
}

func main() {
a, b := swap("Google", "baidu")
fmt.Println(a, b)
}

还可以在定义的时候命名返回值,从函数中可以返回一个命名值。

1
2
3
4
5
6
func rectProps(length, width float64)(area, perimeter float64) {  
area = length * width
perimeter = (length + width) * 2
return // 不需要明确指定返回值,默认返回 area, perimeter 的值
}
/*请注意, 函数中的 return 语句没有显式返回任何值。由于 area 和 perimeter 在函数声明中指定为返回值, 因此当遇到 return 语句时, 它们将自动从函数返回。*/

下划线_在 Go 中被用作空白符,可以用作表示任何类型的任何值。

1
2
3
4
5
6
7
8
func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, _ := swap("Google", "baidu") // 空白符_ 用来跳过不想要的返回结果
fmt.Println(a)
}

4 函数作为另外一个函数的实参

Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。

1
2
3
4
5
6
7
8
9
10
func square(a int) int {
return a*a
}
func swap(a,b int) (int,int){
return b,a
}
func main() {
reta,retb := swap(20,square(5))
fmt.Println(reta,retb)
}

5 闭包和匿名函数

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}

func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}

以上代码执行结果为:

1
2
3
4
5
1
2
3
1
2

八、数组

Go 语言提供了数组类型的数据结构。

数组是同一类型元素的集合,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。例如,整数集合 5,8,9,79,76 形成一个

数组。Go 语言中不允许混合不同类型的元素,例如包含字符串和整数的数组。(当然,如果是 interface{} 类型数组,可以包含任意类

型)

1 声明数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:

1
var variable_name [SIZE] variable_type

以上为一维数组的定义方式。举例:

1
var a [3]int

var a[3]int 声明了一个长度为 3 的整型数组。数组中的所有元素都被自动赋值为数组类型的零值,在这种情况下,a 是一个整型数组,

因此 a 的所有元素都被赋值为 0,即 int 型的零值。运行上述程序将 输出 [0 0 0]

以下定义了数组 balance 长度为 10 类型为 float32:

1
var balance [10] float32

数组的索引从 0 开始到 length - 1 结束。

2 初始化数组

先声明,再赋值:

1
2
var arr [5]int // 声明数组
arr[0] = 1 // 通过索引赋值

声明并赋值:

1
2
var arr = [5]int{1,2,3,4,5} // 全定义
arr := [5]int{1,2,3,4,5} // 简便定义

如果数组长度不确定,可以使用 ...代替数组的长度,编译器会根据元素个数自行推断数组的长度:

1
2
3
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

1
2
//  将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}

初始化数组中 {} 中的元素个数不能大于[]中的数字。

如果忽略[]中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

1
2
// []内可以不写,自动推算
arr := []int{1,2,3,4,5}

数组一经声明 ,长度固定,无法改变。

Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量

进行更改,则不会影响原始数组。

同样,当数组作为参数传递给函数时,它们是按值传递,而原始数组保持不变。

3 访问数组元素

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值(从0开始)。

1
2
arr := []int{1,2,3,4,5}
arr[3] = 100 // 索引取值

4 多维数组

Go 语言支持多维数组,以下为常用的多维数组声明方式:

1
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

以下实例声明了三维的整型数组:

1
var threedim [5][10][4]int

多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:

1
2
3
4
5
a := [3][4]int{  
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, // 这里要有逗号, /* 第三行索引为 2 */
}

5 数组长度

通过将数组作为参数传递给 len 函数,可以得到数组的长度。

1
2
arr := []int{1,2,3,4,5}
fmt.Println(len(arr))

6 数组的遍历

我们可以使用类c语言经典的遍历方法:

1
2
3
4
arr := [5]int{1,2,3,4,5}
for i :=0;i<len(arr);i++{
fmt.Println(i)
}

Go 提供了一种更好、更简洁的方法,通过使用 for 循环的 range 方法来遍历数组。range 返回索引和该索引处的值。让我们使用 range 重写上面的代码。我们还可以获取数组中所有元素的总和。

1
2
3
4
arr := [5]int{11,22,33,44,55}
for index,value :=range arr{ // range函数返回数组的索引,索引处的值
fmt.Println(index,value)
}

如果你只需要值并希望忽略索引,则可以通过用 _ 空白标识符替换索引来执行。

九、包

到目前为止,我们看到的 Go 程序都只有一个文件,文件里包含一个 main 函数和几个其他的函数。在实际中,这种把所有源代码编写在

一个文件的方法并不好用。以这种方式编写,代码的重用和维护都会很困难。而包(Package)解决了这样的问题。

包用于组织 Go 源代码,提供了更好的可重用性与可读性。由于包提供了代码的封装,因此使得 Go 应用程序易于维护。

1 main包

所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中

1
package main

2 创建包

首先,go的所有代码必须放在gopath下的src目录下,包的导入也是从scr开始搜索。

我们在src下新建一个test文件夹,下面创建两个go文件。

1
2
3
4
5
6
- src
- test
1.go
2.go
- abc.go
- bin

两个go文件内部的第一行,定义包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.go
package test // 虽然文件名与包名没有直接关系,但建议包名和文件夹名相同

import "fmt"

func Test1() { // 包内定义的变量或者函数想要给外部包使用,首字母必须大写
fmt.Println("test包下的1.go")
}


// 2.go
package test

import "fmt"

func Test2() { // 包内定义的变量或者函数想要给外部包使用,首字母必须大写
fmt.Println("test包下的2.go")
}

注意:同一个文件夹下的文件只能有一个包名,否则编译报错,这里1.go和2.go的包名必须相同。

当定义了包之后,同一个包的变量名和函数名是唯一的,它们也可以直接使用。

在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。其它包只能访问被导出的函数和变量。

我们把abc.go定义为main包,在main函数内使用定义好的test包:

1
2
3
4
5
6
7
8
9
10
11
12
// abc.go
package main

import "test" // 导入包

func main() {
test.Test1()
test.Test2()
}
// 输出:
test包下的1.go
test包下的2.go

3 init函数

所有包都可以包含一个 init 函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。init 函数的形式如下:

1
2
3
func init() {  

}

init 函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。

包的初始化顺序如下:

  1. 首先初始化包级别(Package Level)的变量
  2. 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。

如果一个包导入了另一个包,会先初始化被导入的包。

尽管一个包可能会被导入多次,但是它只会被初始化一次。

4 其它

导入了包,却不在代码中使用它,这在 Go 中是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包,从而

导致编译时间显著增加。

然而,在程序开发的活跃阶段,又常常会先导入包,而暂不使用它。遇到这种情况就可以使用空白标识符 _

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"test"
)

var _ = test.Name // 错误屏蔽器

func main() {

}

有时候我们导入一个包,只是为了确保它进行了初始化,而无需使用包中的任何函数或变量,这种情况也可以使用空白标识符。

十、Go包管理

1 使用GO111MODULE

在1.11版本后,推出 modules 机制,简称 mod,用于包管理。

使用 Go modules 之前需要环境变量 GO111MODULE,命令行输入go env可以 打印go的环境信息。

  • GO111MODULE=off: 不使用 modules 功能,查找vendor和GOPATH目录
  • GO111MODULE=on: 使用 modules 功能,不会去 GOPATH 下面查找依赖包。
  • GO111MODULE=auto: Golang 自己检测是不是使用 modules 功能,如果当前目录不在$GOPATH 并且 当前目录(或者父目录)下有go.mod文件,则使用 GO111MODULE, 否则仍旧使用 GOPATH mode

我们这里将GO111MODULE设置为on

1
2
set GO111MODULE=on    //windows
export GO111MODULE=on //linux

我们在gopath之外,新建一个demo文件夹作为我们的项目根目录

切换到根目录下执行命令,然后会生成一个 go.mod

1
2
go mod init demo
// 这里的demo换成你文件夹的名字

这里比较关键的就是这个 go.mod 文件,这个文件中标识了我们的项目的依赖的 package 的版本。执行 init 暂时还没有将所有的依赖管

理起来。我们需要将程序 run 起来(比如执行 go run/test),或者 build(执行命令 go build)的时候,才会触发依赖的解析。

这里我们下载一个第三方包作为测试:

1
go get github.com/beego/beego/v2@v2.0.0

当开启了mod模式之后,包不会下载到src目录下

go.mod文件内会有 module 和 require 模块,除此之外还可以包含 replace 和 exclude 模块。

下载的 package 并不是直接存储到 GOPATH/src,而是存储到 GOPATH/pkg/mod 下面。

1
2
3
4
5
6
// go.mod
module demo

go 1.17

require github.com/beego/beego/v2 v2.0.0 // indirect

有indirect注释的代表间接依赖,没有的代表直接依赖,这里是版本号+时间戳+hash

2 自己写的包如何导入

直接在go.mod中写包名即可

3 go vender

1.5 版本推出了 vendor 机制。所谓 vendor 机制,就是每个项目的根目录下可以有一个 vendor 目录,里面存放了该项目的依赖的 package。go build 的时候会先去 vendor 目录查找依赖,如果没有找到会再去 GOPATH 目录下查找。

1
go mod vendor

4 使用代理GoProxy

有些 Golang 的 package 在国内是无法直接 go get 的。

GoProxy 相当于官方提供了一种 proxy 的方式让用户来进行包下载。要使用 GoProxy 只需要设置环境变量 GOPROXY 即可。目前公开的

GOPROXY 有:

  • goproxy.io 在goland–>Go–>Go Modules(ego)中配置这个地址
  • goproxy.cn: 由七牛云提供,参考 github repo

这里介绍Goland使用代理:

网址:https://goproxy.cn/

进入GoLand—>settings—>Go—>Go modules

完成。

十一、切片

Go 语言切片是对数组的抽象,因此切片是引用类型。但自身是结构体,值拷贝传递。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组

相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片本身不拥有任何数据,它是对现有数组的引用。

切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。

1 切片的定义

你可以声明一个未指定大小的数组来定义切片,切片不需要说明长度。

1
2
3
4
5
var 变量名 []类型

// 比如 var str []string
// var arr []int。
// []括号内什么都不写 就是切片类型

或使用 make() 函数来创建切片:

1
2
3
4
5
6
var slice []type = make([]type, len)

// 也可以简写,其中 capacity 为可选参数。这里 len 是数组的长度并且也是切片的初始长度

slice := make([]type, len)
slice := make([]type, length, capacity)

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
func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// 4.初始化赋值
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
s5 := []int{1, 2, 3}
fmt.Println(s5)
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5} // 定义数组
var s6 []int // 定义切片
// 索引区间前闭后开
s6 = arr[1:4]
fmt.Println(s6)
}

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

3 切片初始化

1
s :=[] int {1,2,3 } 

直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3

1
2
3
4
5
6
var arr = [...]int{0,1,2,3,4,5,6,7,8,9,}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1]

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var arr = [...]int{0,1,2,3,4,5,6,7,8,9,}
var slice0 []int = arr[2:8] // 左闭右开,len=high-low
var slice1 []int = arr[0:6] //0可以省略: var slice []int = arr[:end]
var slice2 []int = arr[5:10] //如果切片到结尾,可以省略: var slice[]int = arr[start:]
var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
fmt.Printf("arr %v\n", arr)
fmt.Printf("slice0 %v\n", slice0)
fmt.Printf("slice1 %v\n", slice1)
fmt.Printf("slice2 %v\n", slice2)
fmt.Printf("slice3 %v\n", slice3)
fmt.Printf("slice4 %v\n", slice4)

// 输出:
arr [0 1 2 3 4 5 6 7 8 9]
slice0 [2 3 4 5 6 7]
slice1 [0 1 2 3 4 5]
slice2 [5 6 7 8 9]
slice3 [0 1 2 3 4 5 6 7 8 9]
slice4 [0 1 2 3 4 5 6 7 8]

4 len() 和 cap() 函数

切片是可索引的,并且可以由 len() 方法获取长度。

切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

以下为具体实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var numbers = make([]int,3,5)

printSlice(numbers)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

// 输出
len=3 cap=5 slice=[0 0 0]

5 追加切片元素

正如我们已经知道数组的长度是固定的,它的长度不能增加。 切片是动态的,使用 append 可以将新元素追加到切片上。append 函数的定义是

1
2
3
func appends[]Tx ... T)[]T
// x … T 在函数定义中表示该函数接受参数 x 的个数是可变的。这些类型的函数被称为[可变函数]。
// append :向 slice 尾部添加数据,返回新的 slice 对象。

如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么?

当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。

新切片的容量是旧切片的两倍。

举例:

1
2
3
4
5
6
7
8
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) // capacity of cars is 3
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) // capacity of cars is doubled to 6

// 输出
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6

6 切片的长度和容量

切片的长度是切片中的元素数。

切片的容量是从创建切片索引位置开始的底层数组中元素数。

7 copy() 函数和内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非

常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用

时数组仍然存在内存中。

一种解决方法是使用 [copy] 函数 func copy(dst,src[]T)int 来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被

垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
func countries() []string {
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2] // 创建一个去掉尾部 2 个元素的切片
countriesCpy := make([]string, len(neededCountries))
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
return countriesCpy
}
func main() {
countriesNeeded := countries()
fmt.Println(countriesNeeded)
}

//现在 countries 数组可以被垃圾回收, 因为 neededCountries 不再被引用。

8 切片的函数传递

切片在内部可由一个结构体类型表示。这是它的表现形式,

1
2
3
4
5
type slice struct {  
Length int
Capacity int
ZerothElement *byte
}

切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。因

此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见。

十二、map

1 map集合

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据。Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。

2 创建map

声明一个map集合

1
2
3
var map_variable map[key_data_type]value_data_type
// 比如,声明一个key和value都是字符串类型的集合
var countryCapitalMap map[string]string

上面的例子中,如果声明但不初始化 map,那么就会创建一个 nil mapnil map不能用来存放键值对,如果你添加键值,编译器会提示panic: assignment to entry in nil map错误,所以需要使用 make 函数初始化。

1
2
3
4
5
var countryCapitalMap map[string]string  // 声明一个nil集合
countryCapitalMap = make(map[string]string) // 使用make初始化
countryCapitalMap["k1"] = "v1"
countryCapitalMap["k2"] = "v2"
fmt.Println(countryCapitalMap)

可以使用类型推导,省去声明的步骤,直接用make函数创建集合

1
2
3
map_variable := make(map[key_data_type]value_data_type)
// key_data_type是key的类型
// value_data_type是value的类型

你也可以在声明时初始化集合,比如

1
2
3
4
money := map[string]int {
"steve": 12000,
"jamie": 15000,
}

3 添加元素

你可以使用中括号为map赋值,要注意key不能重复

1
2
countryCapitalMap["k1"] = "v1"
countryCapitalMap["k2"] = "v2"

也可以声明时初始化赋值

1
2
3
4
5
status := map[int]string{
200: "OK",
404: "Not Found",
}
fmt.Println(status)

4 获取元素

获取map中元素的方法很简单,使用中括号取值即可。

1
2
3
4
5
6
7
8
status := map[int]string{
200: "OK",
404: "Not Found",
}
//res := status[200]
res := status[404]
fmt.Println(status)
fmt.Println(res)

如果获取一个不存在的元素,会返回一个0而不会返回错误。

1
2
3
4
5
student := make(map[string]int)
student["mike"] = 18
student["lily"] = 20
student["bob"] = 22
fmt.Println(student["bbb"])

要查看元素在集合中是否存在,可以这样做:

1
2
3
value, ok := student["bbb"]
fmt.Println(value) // 返回'bbb'对应的值,如果不存在返回0
fmt.Println(ok) // 如果key存在返回true,key不存在返回false

5 遍历map

可以使用for range语法遍历map,但是每次遍历都是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用哈希表来实现的。

1
2
3
4
5
6
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
for country, capital := range countryCapitalMap {
fmt.Print(country)
fmt.Print(capital)
fmt.Print("\n")
}

6 delete函数删除元素

要删除map中的元素,使用delete函数,语法是 delete(map, key)

1
2
3
4
5
6
student := make(map[string]int)
student["mike"] = 18
student["lily"] = 20
student["bob"] = 22
delete(student, "mike") // 删除key为mike的元素
fmt.Println(student)

7 获取map的长度

使用len函数获取map长度

1
2
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
fmt.Println(len(countryCapitalMap))

8 map的函数传递

集合map可以作为参数传递。map和切片一样是引用类型,当map作为参数传递给函数时,即使是值传递,也将引用相同的底层数据结构,函数内所做的更改也会在函数外可见。

1
2
3
4
5
6
7
8
9
10
11
12
func add(m map[string]int) {
for key, value := range m {
m[key] = value + 1
}
}

func main() {
people := map[string]int{"mike": 18, "lily": 17, "bob": 15}
add(people)
fmt.Println(people)
// 函数内的更改,影响原来的元素
}

十三、指针

相比于c或c++,go语言的指针是很容易学习的,它可以更简单地执行一些任务。

1 取地址操作符&

变量是一种使用方便的占位符,用于引用计算机内存地址,使用&符号获取变量的内存地址,如下:

1
2
3
4
5
func main() {
var str = "hello world"
fmt.Println(&str)
// 返回结果为0xc00004c230
}

2 指针的定义

指针是一个变量,这个变量存储了另一个变量的内存地址。

在使用指针前,你需要声明指针。指针声明格式如下

1
2
3
var var_name *var-type
// var_name 为指针变量名
// var-type 为指针类型

举例:

1
2
var ptr1 *string // 指向字符串
var ptr2 *int // 指向整型

当一个指针被定义后没有分配到任何变量时,它的初始值为nil,也被称为空指针

1
2
3
4
5
6
func main() {
var ptr *int
fmt.Println(ptr) // 未分配的指针初值为<nil>
fmt.Println(&ptr) // 指针作为一个变量,也可以取到它的内存地址
fmt.Println(*ptr) // 未分配的指针,获取指针指向的内容时会报错panic: runtime error: invalid memory address or nil pointer dereference
}

3 使用指针

定义指针后,需要给指针变量赋值,使用*操作符可以获取指针指向的内容(也称解引用)。

1
2
3
4
5
6
7
8
func main() {
var ptr *int // 声明指针变量,它指向一个整型变量,初始化一个空指针
b := 3 // 创建变量b为整型变量
ptr = &b // 将变量b的内存地址赋值给指针,也称'指针ptr指向了b'
fmt.Println(ptr)
fmt.Println(b)
fmt.Println(*ptr) // 指针解引用,获取指针所指向的变量的值
}

4 指针的函数传递

指针也是一个变量,可以向函数传递。

1
2
3
4
5
6
7
8
9
func add(ptr *int) {
*ptr++
}
func main() {
a := 10
var ptr *int = &a
add(ptr)
print(a)
}

5 指针运算

与c和c++不同,go中的指针不支持对指针运算,比如指针的递增和递减。

1
2
3
a := 10
var ptr *int = &a
ptr++ // 不支持指针运算,返回错误invalid operation: ptr++ (non-numeric type *int)

6 多级指针

已知指针本身也是变量,指针存放的是变量的内存地址。所以,可以让一个指针存放另一个指针变量的地址,此时称这个指针变量为指向指针的指针(二级指针)。

1
2
3
4
5
6
7
8
9
a := 10
var ptr *int = &a
var pptr **int
pptr = &ptr
fmt.Println(a) // 10
fmt.Println(ptr) // 0xc0000160b8
fmt.Println(pptr) // 0xc000006028
fmt.Println(*pptr) // 0xc0000160b8
fmt.Println(**pptr) // 10

如上面的例子,二级指针解引用时,需要使用两个*

同理,你可以“无限套娃”下去,定义多级指针。

下面是一个三级指针、四级指针的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
a := 10
var ptr *int = &a
var pptr **int
var ppptr ***int
var pppptr ****int
pptr = &ptr
ppptr = &pptr
pppptr = &ppptr
fmt.Println(a)
fmt.Println(ptr)
fmt.Println(pptr)
fmt.Println(ppptr)
fmt.Println(pppptr)

十四、结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。比如你需要定义一些互相有联系的变量,而变量之间的类型不同。

举个实际的例子,保存图书馆的书籍记录,每本书有以下属性:titleauthorprice,此时就可以使用结构体类型。

1 定义结构体

结构体定义需要使用typestruct语句。struct语句定义一个新的数据类型,结构体中有一个或多个成员。type语句设定了结构体的名称。

1
2
3
4
5
6
7
8
type struct_variable_type struct {
member definition
member definition
...
member definition
}
// struct_variable_type为自定义的结构体类型名称
// 在结构体内部定义成员(member definition)

一旦定义了结构体,就可以将它作为变量的声明

1
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

示例:

1
2
3
4
5
6
7
8
9
type Books struct {
title string
author string
price int
}

func main() {
var book Books // 声明变量为Books结构体类型
}

2 访问结构体成员

果要访问结构体成员,需要使用点. 操作符。

1
2
3
4
5
6
7
8
9
10
type Books struct {
title string
author string
price int
}

func main() {
var book Books // 声明但未初始化赋值
fmt.Println(book.price) // 未赋值时,结构体为零值
}

3 结构体成员赋值

可以在声明时赋值;也可以先声明,再使用.操作符赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Books struct {
title string
author string
price int
}
func main() {
var book = Books{
title: "西游记",
author: "吴承恩",
price: 60,
}
// 也可以如下方式赋值
//book.title = "西游记"
//book.author = "吴承恩"
//book.price = 60
fmt.Println(book)
}

你可以为结构体的部分成员赋值,此时剩下的成员会被赋值为零值。

4 结构体的导出

定义结构体及其成员时,仍遵循大写导出,小写不导出的规则。如下:

1
2
3
4
5
type Books struct { // 大写结构体名字,可以导出,外部包可以访问
title string // 小写成员名字,不能被导出,外部包无法访问
author string // 小写成员名字,不能被导出,外部包无法访问
Price int // 大写成员名字,可以导出,外部包可以访问
}

如果想要让这个结构体所有成员被外部访问,需要将它们大写。

5 结构体的函数传递

你可以像其他数据类型一样将结构体类型作为参数传递给函数,并且可以访问内部成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Books struct {
Title string
Author string
Price int
}

func change(book Books) {
book.Title = "红楼梦"
book.Author = "曹雪芹"
book.Price = 50
}
func main() {

var book = Books{
Title: "西游记",
Author: "吴承恩",
Price: 60,
}
fmt.Println(book)
change(book) // 结构体按值传递,在函数内部的更改不影响原来的值
fmt.Println(book)
}

在函数内修改结构体,原来的结构体不会改变。

6 结构体指针与结构体的函数引用传递

要想在函数内更改结构体,可以利用结构体指针。相当于结构体的引用传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Books struct {
Title string
Author string
Price int
}

func change(book *Books) {
book.Title = "红楼梦"
book.Author = "曹雪芹"
book.Price = 50
}
func main() {

var book = Books{
Title: "西游记",
Author: "吴承恩",
Price: 60,
}
var ptr *Books = &book // 定义结构体指针,指向book
fmt.Println(book)
change(ptr) // 传递指针,通过指针修改原结构体
fmt.Println(book) // 此时是引用传递,函数内的更改在外部生效
}

7 匿名结构体

我们之前定义的结构体称为命名结构体,因为我们创建了新的类型;匿名结构体是是一个变量,不创建新的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 匿名结构体
var Book struct {
title string
author string
price int
}
// 结构体赋值
Book.title = "西游记"
Book.author = "吴承恩"
Book.price = 50
fmt.Println(Book)
}

也可以在声明同时赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 创建匿名结构体,使用变量Book接收它,并为它的成员赋值
Book := struct {
title string
author string
price int
}{
title: "西游记",
author: "吴承恩",
price: 50,
}
fmt.Println(Book)
}

8 结构体嵌套

结构体内部还可以定义结构体。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Book struct {
Title string
Author struct {
Name string
Age int
}
price int
}

func main() {
book := Book{
Title: "西游记",
Author: struct {
Name string
Age int
}{
Name: "吴承恩",
Age: 18,
},
price: 60,
}
fmt.Println(book)
}

9 匿名字段

定义结构体时,成员字段可以只有类型而没有字段名,这种形式的结构体字段为匿名字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义匿名字段
type Book struct {
string
int
}

func main() {
book := Book{
string: "红楼梦",
int: 60,
}
// 如果字段没有名称,那么默认它的名称为它的类型
fmt.Println(book)
}

如果字段没有名称,那么默认它的名称为它的类型。比如string字段和int字段。

10 结构体比较

结构体是值类型(通过函数传递也可以看出来),如果它的所有字段都可比较,则该结构体也是可比较的;如果结构体包含不可比较的字段,则结构体也不可比较。

11 字段提升

如果结构体中有匿名结构体类型的字段,该匿名结构体里的字段就称为提升字段。这些字段自动提升,可以用外部结构体直接访问。

1
2
3
4
5
6
7
8
9
type Book struct {
title string
author // 匿名字段
}

type author struct {
name string
age int
}

如上,Book中有一个匿名字段authorauthor是结构体类型,包含nameage两个成员,那么我们通过Book就可以访问到它们。

十五、方法

1 方法定义

go语言中同时有函数和方法。一个方法就是一个包含了接收器的函数,接收器一般是结构体类型中的字段,也可以是非结构体。它的定义如下:

1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}

通俗来说就是定义一个函数,指定一个接收器来接收这个函数。

通过一个示例学习方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Book struct {
title string
price int
}

func (book Book) printprice() {
fmt.Println(book.price)
book.price += 2
}
func main() {
book := Book{
title: "红楼梦",
price: 58,
}
book.printprice()
fmt.Println(book.price)
}

在上面的示例中,我们定义一个结构体含两个字段;然后定义一个函数,指定接收器为book,此时相当于在Book类型上绑定了一个方法。我们在main函数中可以通过.操作符调用这个方法。

2 值接收器和指针接收器

在上面的示例中我们发现,printprice方法想让price自增2,但实际上原结构体并不会被改变,这是因为我们使用了值接收器。

我们如果想要在方法内更改字段并使得它对调用者可见,需要使用指针接收器。比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Book struct {
title string
price int
}

func (book *Book) printprice() {
fmt.Println(book.price)
// 这里传递的是指针,但是我们仍然可以取到字段。这是go内部自动转化的结果,如果在c或c++中,你需要先对指针解引用
fmt.Println((*book).price)
book.price += 2
}
func main() {
book := Book{
title: "红楼梦",
price: 58,
}
book.printprice()
fmt.Println(book.price)
}

使用指针接收器后,在 printprice 方法中对 Book 结构体的字段 price 所做的改变对调用者是可见的,换句话说在方法内修改值,原来的值也会随之改变。

另外补充一点,如果你有c编程的经验,你可能注意到,在操作结构体的字段时,如果在c或c++中,需要先对指针解引用;而在go中不同,指针直接使用.操作符就可以取到字段。

另外,go可以直接使用指针操作,而c/c++不可以,对比c++代码来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Book {
string title;
int price;
};
void printprice(Book* book){
(*book).price +=2; // c 解引用
cout <<book->price<<endl;// c++ 解引用
}
int main() {
Book book = {"红楼梦",58};
printprice(&book);
return 0;
}

那么该如何选择值接收器与指针接收器?如果你想要方法所做的改变对调用者可见,那么就使用指针接收器;否则就使用值接收器。另外,值接收器需要对原结构体拷贝,如果结构体字段很大,那么付出的内存消耗也大。

3 字段方法提升

和结构体的字段提升一样,接收器是匿名字段的结构体,可以直接在外部使用它的方法。

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
type Book struct {
title string
price int
Author
}

type Author struct {
name string
age int
}

func (a Author) getage() {
// 接收器为Author
fmt.Println(a.age)
}

func main() {
book := Book{
title: "红楼梦",
price: 58,
Author: Author{
name: "曹雪芹",
age: 20,
},
}
book.getage() // 可以直接访问Author的绑定方法
fmt.Println(book.price)
}

十六、接口

1 定义接口

在面向对象中,接口规范了一个对象的行为,接口指定对象必须实现的方法,而实现的细节由对象自己去完成。go语言中同样提供接口,它把所有的具有共性的方法定义在一起,只要任何其他类型只要实现了这些方法就是实现了这个接口。

接口的定义语法如下:

1
2
3
4
5
6
7
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

接口是方法的集合,要实现这个接口就必须实现这些方法。示例:

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
// 定义接口
type Animal interface {
sleep()
eat()
}

// 定义结构体
type Dog struct {
Name string
Age int
}

// 为Dog绑定方法实现接口
func (dog Dog) sleep() {
fmt.Println(dog.Name + "is sleeping!")
}

//为Dog绑定方法实现接口
func (dog Dog) eat() {
fmt.Println(dog.Name + "is eating!")
}

func main() {
d := Dog{
Name: "小狗",
Age: 5,
}
d.eat()
d.sleep()
}

上面定义了一个Animal接口,要求实现两个方法。然后定义一个Dog结构体,为它绑定这两个方法,go与java不同,不需要显式声明实现的接口,只要这个类型实现了接口的所有方法,就默认实现了这个接口。但是,这样定义与之前为结构体绑定方法没有区别,把接口去掉程序仍然可以正常工作。

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
33
34
35
// 定义接口
type Animal interface {
eat()
}

// 定义结构体
type Dog struct {
Name string
Age int
}
type Bird struct {
Name string
}

//为Dog绑定方法实现接口
func (dog Dog) eat() {
fmt.Println(dog.Name + "只吃肉")
}

//为Bird绑定方法实现接口
func (bird Bird) eat() {
fmt.Println(bird.Name + "吃虫子")
}

func main() {
// 定义nil接口
var animal Animal

animal = Bird{Name: "麻雀"}
animal.eat() //麻雀吃虫子

animal = Dog{Name: "小狗"}
animal.eat() //小狗只吃肉

}

接口是动态的,就像切片是对数组的动态引用一样,接口也是类似的工作方式。

上面的程序中,我们定义了Animal接口,且BirdDog都实现了这个接口,然后我们在main函数中,先定义一个未赋值的接口。首先将Bird指定给它,此时animal的动态类型就是Bird,此时访问animal接口时,只会返回动态值的类型(eat方法),它的静态类型(Name字段)保持隐藏。然后,我们将Dog指定给它,此时的动态类型就是Dog,我们访问的eat方法就是绑定给Dog的方法,由此实现了多态。

3 类型断言

先介绍类型断言的语法:

1
value, ok := i.(Type)

上节提到接口不能访问低层的静态字段,我们可以使用类型断言来获取这些基础值。

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
// 定义接口
type Animal interface {
eat()
}

// 定义结构体
type Dog struct {
Name string
Age int
}
type Bird struct {
Name string
}

//为Dog绑定方法实现接口
func (dog Dog) eat() {
fmt.Println(dog.Name + "只吃肉")
}

//为Bird绑定方法实现接口
func (bird Bird) eat() {
fmt.Println(bird.Name + "吃虫子")
}

func main() {
// 定义接口
var animal Animal
animal = Dog{Name: "小狗", Age: 5}
// 类型断言,判断animal的具体类型是否为Dog
value, ok := animal.(Dog)
fmt.Println(ok)
// 断言成功,获取底层字段内容
fmt.Println(value.Age)
fmt.Println(value.Name)

}

其中,类型断言如果正确,就将此时的底层值赋值给animal,将true赋值给ok;反之,animal为该类型的零值,ok=false

4 多接口

一个类型可以实现多个接口。例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义接口
type Animal interface {
eat()
}

type Habit interface {
football()
}

// 定义结构体
type Dog struct {
Name string
}

// 实现Animal接口
func (dog Dog) eat() {
fmt.Println(dog.Name + "只吃肉")
}
// 实现Habit接口
func (dog Dog) football() {
fmt.Println(dog.Name + "爱玩足球")
}

上面例子中,Dog实现了两个接口,也就意味着可以将Dog类型的值赋给AnimalHabit接口类型的变量。

5 空接口

当接口没有方法时,它被称为空接口。 除了有名的空接口,还可以定义匿名的。

匿名空接口的定义为 interface{},一般用于函数的形式参数。空接口不规范任何方法,也就意味着所有类型都实现了空接口。举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
type str string

// 函数接收一个空接口
func foo(in interface{}) {
fmt.Println(in)
}

func main() {
s := str("hello world")
i := 5
foo(s)
foo(i)
}

我们自定义一个str类型,还有一个int类型的数据,将它传递给foo,由于任何类型都实现空接口,空接口作为函数的参数可以保存任何类型的值,所以这个函数可以接收所有类型的数据。

6 接口嵌套

顾名思义,举例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义接口
type Animal interface {
eat()
}

type Habit interface {
football()
}
type Dog interface {
sleep()
Animal
Habit
}

定义的Dog接口里,包含AnimalHabit,所以要实现Dog必须实现这些接口(类似于面向对象的继承)

十七、面向对象

go不是一个完全面向对象的语言,甚至没有类的概念。但是,这并不意味着go不支持面向对象,因为面向对象只是一种实现形式,只要实现了封装继承和多态,在使用上与其他面向对象语言就没有不同。

在go中,可以使用结构体取代类,使用接口实现多态,使用匿名字段提升实现继承。所以,go是完全可以做到面型对象编程的,毕竟面向对象只是编程思想。

1
2
3
4
5
6
7
8
type Animal struct {
name string
age int
}

func New(name string, age int) Animal {
return Animal{name: name, age: age}
}

我们可以这样定义,定义一个结构体,New函数返回这个结构体类型,直接调用New创建对象,达到与java等语法类似的效果。

十八、并发编程

1 并发和并行

并发是指多个事件在同一时间间隔内发生,或者说并发是具有同时处理多个任务的能力。

并行是指多个事件在同一时刻发生,注意同一时刻同一时间间隔的区别。并发在宏观上看上去,一段时间内多个任务同时执行,但是在每一时刻,单处理机只能有一道程序执行,只是操作系统会不断的切换多个任务。

举个例子,如果你在0-1分钟切菜,1-2分钟洗水果,2-3分钟切菜,那么在0-3分钟的时间间隔里,切菜和洗水果是并发的;如果你在0-3分钟的每一时刻,左手切菜,右手洗水果,那么这两个动作就是并行的。下图可以帮助你更好地理解:

762938-20190816145445091-483415977

2 go协程

线程又被称为轻量级进程,它是程序执行的最小单元,线程不拥有系统资源(只拥有一点运行中必须的资源),它可以与同属于一个进程的其他线程共享进程资源,线程的切换由操作系统调度。

协程又称微线程,协程运行在用户态下,由于在进程切换时会消耗很多资源,协程则避免了这个问题,协程完全由程序自己控制,操作系统并不会感知。在go中,创建一个go协程的开销很小,你可以创建千百个协程并发运行。

go协程称为goroutine,go通过goroutine支持并发,goroutine是go中最基本的执行单元。

生成一个goroutine的方式非常简单,使用go关键字就创建了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"fmt"
"time"
)

func main() {
// 创建10个goroutine
for i := 0; i < 10; i++ {
go func() {
fmt.Println("创建一个goroutine")
}()
}
time.Sleep(3 * time.Second)
}

我们创建了10个go协程,它们与main函数并发执行,main函数运行在主协程上。正如上文所说,go协程的开销很小(几kb),所以可以创建很多goroutine,多个goroutine调度的顺序是乱序的。

另外,在主进程中我们sleep等待了三秒,如果不等待会发生什么呢?尝试把它去掉,你会发现程序什么都没有输出。这就是go协程的性质,主协程不会等待go协程执行完毕。在调用协程之后,主协程会立即执行代码的下一行,忽略该协程的任何返回值。如果主协程结束运行,则其他协程也不会继续运行。所以我们暂且使用sleep等待其他协程执行完毕,才结束主协程,这样就能看到输出。

3 goroutine的GMP调度模型

goroutine的整体调度模型如下图:

93e25d0e626a8263f48ae82cab9587ed

其中:

  • G指goroutine,在全局有一个Global队列,每当创建一个goroutine就把它入队
  • M指machine,它代表一个用户级线程,所有的G任务,最终还是在M上执行
  • P指processor处理器,它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine。它处在G和M之间,直接与M交互。P默认就是CPU可用的核数,可以通过runtime.GOMAXPROCS修改(一般不修改)。

调度的过程是这样的,当通过go关键字创建一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M需要持有(绑定)一个P,接着M会启动一个内核级线程,循环从P的本地队列里取出一个goroutine并执行。当M执行完了当前P的Local队列里的所有G后,P会先尝试从Global队列寻找G来执行,如果Global队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。

4 channel定义

channel(信道/通道)是go的一种重要特性,类似于管道,其本质是一个先进先出的队列,它实现了goroutine之间的通信,多个goroutine可同时修改一个channel。

channel是一个变量,使用chan来定义。

1
2
var 变量名 chan 类型
// channel是有类型的,比如int类型的channel只能存放整型

channel是引用类型,和其他引用类型一样,channel 的空值为nil。channel一定要初始化后才能进行读写操作,否则会永久阻塞,所以一般使用make创建channel。

1
ch := make(chan int)

5 使用channel进行通信

初始化一个channel,就可以在里面取值放值,

1
2
3
4
5
ch := make(chan int)
// 放值
ch <- 3
// 取值
<- ch

默认情况下向channel发送数据和接收数据会阻塞,举例:

1
2
3
4
5
6
7
8
9
10
11
12
func foo(ch chan int) {
fmt.Println("hello world")
// 阻塞,直到有其它goroutine从ch放数据
a := <-ch
fmt.Println(a)
}
func main() {
ch := make(chan int)
go foo(ch)
// 向channel中放一个值,阻塞,直到有其它goroutine从ch取到数据
ch <- 3
}

读取数据推荐方法:

1
2
a, ok := <-ch
// 如果取到,ok=true;反之为false

6 关闭channel

go提供了内置的close函数对channel进行关闭操作。

1
2
3
ch := make(chan int)

close(ch)

注意以下几点:

  • 关闭一个未初始化(nil) 的channel会产生panic

  • 重复关闭同一个channel会产生 panic

  • 向一个已关闭的channel中发送消息会产生 panic

  • 从已关闭的channel读取消息不会产生panic,且能读出channel中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的channel中读取消息永远不会阻塞,并且会返回一个为 false的ok-idiom,可以用它来判断 channel 是否关闭。

  • 关闭channel会产生一个广播机制,所有向channel读取消息的goroutine都会收到消息。接收方可以用ok来检查信道是否已经关闭

7 死锁

当go协程给一个channel发送数据时,如果没有其他go协程来接收数据,就会形成死锁。同理,当有go协程等待从一个channel接收数据时,如果没有人发送,也会死锁。

8 单向channel

channel类型是可以带有方向的:

1
2
3
var readOnlyChan <-chan int        // 只读chan,只能读channel,不可写入
var writeOnlyChan chan<- int // 只写chan,只能写channel,不可读
var ch chan int //读写channel

可能你有疑问,前面说过不管读写都会阻塞,如果定义只读或者只写channel不会发生死锁吗?实际上,单独定义确实没有意义,所以一般在主goroutine中定义读写channel,然后在一些函数参数传递时候指明可读还是可写。比如:

1
2
3
4
5
6
7
8
9
10
func foo(ch <-chan int) { // 定义只读channel 只负责读,不能写
fmt.Println("hello world")
a, ok := <-ch
fmt.Println(a, ok)
}
func main() {
ch := make(chan int)
go foo(ch)
ch <- 3
}

9 遍历channel

可以使用for range遍历信道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("存放", i)
}
close(ch)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("接收", v)
}
}
1
2
3
4
5
6
7
8
9
10
11
// 上述代码执行结果为:
存放 0
接收 0
接收 1
存放 1
存放 2
接收 2
接收 3
存放 3
存放 4
接收 4

可以看到,for range一直从信道中尝试接收数据,如果信道没有关闭也没有数据就阻塞等待,一旦某个goroutine发送数据,就立刻取出。直到信道关闭时,循环会自动结束。

10 缓冲channel

带缓冲区channel:定义声明时候制定了缓冲区长度,可以保存多个数据。只在缓冲已满的情况,发送数据时才会阻塞;同样,只有在缓冲为空的时候,接收数据才会阻塞。

不带缓冲区channel:发送数据就阻塞,直到有goroutine读;读取时也会阻塞,直到有goroutine写。

定义一个带缓冲的channel很简单:

1
2
3
4
ch := make(chan type, [capacity])// 创建channel信道的语法,capacity可选,如果不写默认为0
// 举例
ch := make(chan int) // 不带缓冲区,此时capacity为0
ch := make(chan int,10) // 带缓冲区

要让一个信道有缓冲,上面语法中的 capacity 应该大于 0,一旦定义了缓冲channel,在缓冲满之前不会阻塞。

11 waitgroup

使用waitgroup可以实现同步,即等待所有的goroutine执行完成。举个例子:

1
2
3
4
5
6
7
8
9
10
func foo(i int) {
fmt.Printf("foo执行了%d次\n", i+1)

}
func main() {
for i := 0; i < 5; i++ {
go foo(i)
}

}

如果直接执行,主协程不会等待go协程执行完毕,所以看不到任何输出。我们可以定义一个waitgroup

1
2
var wg sync.WaitGroup
// 它不是引用类型,当作参数传递时是值传递,要想修改原来的值需要传指针(地址)

WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。我们在上述代码的基础上使用WaitGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
func foo(i int, wg *sync.WaitGroup) {
fmt.Printf("foo执行了%d次\n", i+1)
wg.Done() // Done() 表示把计数器-1
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // Add(1)表示把计数器+1
go foo(i, &wg) // 使用指针类型才可以修改原值
}
wg.Wait() // wait() 会阻塞代码运行,直到计数器的值减为0

}

输出结果为

1
2
3
4
5
6
// 顺序是不固定的
foo执行了5
foo执行了1
foo执行了4
foo执行了2
foo执行了3

另外,Add(delta int)函数中,参数delta可以为负值;但是如果计数器为负值,会提示panic: sync: negative WaitGroup counter 错误。

12 Mutex

go提供了mutex用多个goroutine的互斥访问。互斥问题的场景很多,比如并发更新同一个资源、并发更新计数器、秒杀活动等等,这些都需要保证数据准确。首先要明确临界资源的概念。不论是进程、线程还是goroutine,互斥问题的概念都是相通的:我们把一次仅允许一个goroutine使用的资源称为临界资源。为了保证临界资源的正确使用,可以划分为四部分

  • 进入区:在进入临界区之前,负责阻止其它goroutine同时进入临界区
  • 临界区:使用临界资源
  • 退出区:某个goroutine退出临界区之后,负责更改临界区状态,允许其它goroutine访问临界区
  • 剩余区:其余部分

实现互斥访问的方式有很多,mutex是互斥锁,可确保在某时刻只有一个goroutine在临界区运行,以防止出现竞态条件。

在go的标准库中,package sync提供了锁相关的一系列同步原语,还定义了一个Locker的接口,mutex就实现了这个接口。

1
2
3
4
type Locker interface {
Lock()
Unlock()
}

使用mutex解决互斥问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func incr(wg *sync.WaitGroup, m *sync.Mutex) { // mutex需要引用传递
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var x = 0
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 100; i++ {
w.Add(1)
go incr(&w, &m) // 传递引用
}
w.Wait()
fmt.Println("x结果是", x)
}

十九、错误、异常处理

本章吐槽:go的异常处理实在是太难用了,一度让我写得十分反胃。

1 error

go中我们通常会在函数或方法中返回error结构对象来判断是否有异常出现。go内置的错误类型error是一个接口类型,自定义的错误类型必须实现它:

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

使用errors.New可返回一个错误信息:

1
err := errors.New("错误消息")

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("被开方数不能为负")
}
return math.Sqrt(f), nil
}

func main() {
d, err := Sqrt(-3)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(d)
}

在上面代码中,我们定义一个简单的开方函数,如果被开方数为负就输出一个错误。

一般按照如下方法使用:

  • 如果函数需要处理异常,通常将error作为多值返回的最后一个值,返回的error值为nil则表示无异常,非nil则是有异常。
  • 一般先用if语句处理error!=nil,把正常逻辑写在后面。

另外你会发现,返回错误并不会终止程序的运行。

2 panic

error是错误,panic是异常。错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中 ;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。与error不同,panic会终止程序的运行,在执行完所有的defer函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前goroutine的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。

1
2
3
4
5
6
func main() {
fmt.Println("1")
fmt.Println("2")
panic("发生异常") // 主动抛异常,类似于java的throw,python的raise
fmt.Println("3")
}

上述代码中,使用panic抛出异常后,第三个打印语句不会执行。

3 defer

defer是go语言中的延迟执行语句,用来添加函数结束时执行的代码。

1
2
3
4
5
6
7
8
9
10
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
}
/*结果为:
3
2
1
*/

当一个函数内多次调用defer时,go会把defer调用放入到一个栈中,在函数结束之前依次出栈,因此输出结果是逆序。

与panic结合使用:

1
2
3
4
5
6
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
panic("发生异常") // 主动抛异常,类似于java的throw,python的raise
fmt.Println("3")
}

在抛出异常之前,再逆序执行所有的defer。

4 recover

panic会使后面的语句终止,recover可以让程序能正常运行下去。只有在defer函数的内部,调用 recover才有用。在defer函数内调用 recover,可以取到 panic的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。recover的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func f1() {
fmt.Println("f1")
}

func f2() {
defer func() { // 匿名函数,延迟执行
if err := recover(); err != nil {
fmt.Println(err) // 如果出错,恢复错误继续执行,并且打印错误
}
fmt.Println("无论是否有异常,此处永远都会执行")
}()

fmt.Println("f2")
panic("抛出异常")

}

func main() {
f1()
f2()
}