前言:

这是golang初学者

CrispCookie

根据找的教程pdf归纳出的markdown笔记(写的比较粗糙), 写的时候发现超过2w字typora就有明显卡顿了, 太逊了。go是一门比较缝合怪<杂糅了各种高级语言的语法>的一门语言,有着python的切片, c的指针, java的GC机制等等语法特性, 并且还有着一些让人两眼一黑的语法, 比如开头字母大写表示public, 小写表示private, 结构体的继承语法, 都让初学者学习过程颇有趣味性。当然go的优点和潜力也显而易见, 多协程的低内存消耗一骑绝尘等, 我也希望通过go将来能有碗饭吃

环境搭建

GO语言mac环境搭建

下载安装Golang

Go 官网下载地址:https://golang.org/dl/

Go 官方镜像站(推荐):https://golang.google.cn/dl/

安装软件

1.双击下一步下一步进行安装

image-20221031140247149

2.验证安装是否成功

1
go version

3.查看go环境

1
go env

说明: Go1.11 版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项 目放到 GOPATH 指定目录下,你可以在你磁盘的任何位置新建一个项目. Go1.13 以后可以彻底不要 GOPATH了

下载goland, 简单省事无脑


变量声明

变量的来历

程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存 上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性 会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接 通过这个变量就能找到内存上对应的数据了

变量类型

变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个 多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型浮点型布尔型等。 Go 语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用

GO 语言中变量的声明

Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。Go 语言中关键字 和保留字都不能用作变量名。 Go 语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且 Go 语言的变 量声明后必须使用

var 声明变量

1
var 变量名称 type
1
2
3
var name string
var age int
var isOk bool
1
2
3
4
5
6
package main
import "fmt";
func main() {
var username="张三" var age int =20
fmt.Println(username,age)
}

一次定义多个变量

1
var identifier1, identifier2 type
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
var username, sex
username = "张三"
sex = "男"
fmt.Println(username, sex)
}
//申明变量的时候赋值
var a, b, c, d = 1, 2, 3, false

批量声明变量的时候指定类型

1
2
3
4
5
6
7
8
var (
a string
b int
c bool
)
a = "张三" b = 10
c = true
fmt.Println(a,b,c)

批量声明变量并赋值

1
2
3
4
5
6
7
var (
a string = "张三"
b int = 20
c bool = true
)
fmt.Println(a, b, c)
fmt.Println(a,b,c)

变量的初始化

Go 语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被 初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为 0。 字符串变量的默认值 为空字符串。 布尔型变量默认为 false。 切片、函数、指针变量的默认为 nil。 当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

1
var 变量名 类型 = 表达式

举个例子:

1
2
var name string = "zhangsan" 
var age int =18

或者一次初始化多个变量并赋值

1
var name, age = "zhangsan", 20

类型推导

有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完 成初始化

1
2
var name = "Q1mi" 
var age = 18

短变量声明法

在函数内部,可以使用更简略的 := 方式声明并初始化变量。

注意:短变量只能用于声明局部变量,不能用于全局变量的声明

1
变量名 := 表达式
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
// 全局变量 m
var m = 100

func main() {
n := 10
m := 200 // 此处声明局部变量 m
fmt.Println(m, n)
}

使用变量一次声明多个变量,并初始化变量

1
2
m1, m2, m3 := 10, 20, 30
fmt.Println(m1, m2, m3)

匿名变量

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable). 匿名变量用一个下划线_表示,例如:

1
2
3
4
5
6
7
8
9
func getInfo() (int, string) {
return 10, "张三"
}

func main() {
_, username := getInfo()
fmt.Println(username)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明

注意事项:

1、函数外的每个语句都必须以关键字开始(var、const、func 等)

2、:=不能使用在函数外。

3、_多用于占位,表示忽略值。

Go 语言中的常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的 声明和变量声明非常类似,只是把 var 换成了 const,常量在定义的时候必须赋值

使用 const 定义常量

1
2
const pi = 3.1415
const e = 2.7182

声明了 pi 和 e 这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了

多个常量也可以一起声明:

1
2
3
4
const (
pi = 3.1415
e = 2.7182
)

const 同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如

1
2
3
4
5
const (
n1 = 100
n2
n3
)

上面示例中,常量 n1、n2、n3 的值都是 100


基本数据类型

Golang 数据类型介绍

Go 语言中数据类型分为:基本数据类型和复合数据类型

基本数据类型有: 整型、浮点型、布尔型、字符串

复合数据类型有: 数组、切片、结构体、函数、map、通道(channel)、接口等

整形

整型分为以下两个大类:

有符号整形按长度分为:int8、int16、int32、int64

对应的无符号整型:uint8、uint16、uint32、uint64

无符号所能表示的数据长度时对应有符号的两倍

类型 范围 占用空间 有无符号
int8 (-128 到 127) -2 ^7 到 2 ^7 -1 1 个字节
int16 (-32768 到 32767) -2 ^15 到 2 ^15 -1 2 个字节
int32 (-2147483648 到 2147483647) -2 ^31 到 2 ^31 -1 4 个字节
int64 (-9223372036854775808 到 9223372036854775807) -2 ^63 到 2 ^63 -1 8 个字节
uint8 (0 到 255) 0 到 2 ^8 -1 1 个字节
uint16 (0 到 65535) 0 到 2 ^16 -1 2 个字节
uint32 (0 到 4294967295) 0 到 2 ^32 -1 4 个字节
uint64 (0 到 18446744073709551615) 0 到 2 ^64 -1 8 个字节

关于字节 字节也叫 Byte,是计算机数据的基本存储单位。8bit(位)=1Byte(字节) 1024Byte(字节)=1KB 1024KB=1MB 1024MB=1GB 1024GB=1TB 。在电脑里一个中文字是占两个字节的

特殊整形

类型 描述
uint 32 位操作系统上就是 uint32,64 位操作系统上就是 uint64
int 32 位操作系统上就是 int32,64 位操作系统上就是 int64
uintptr 无符号整型,用于存放一个指针

注意: 在使用 int 和 uint 类型时,不能假定它是 32 位或 64 位的整型,而是考虑 int 和 uint 可能在不同平台上的差异

注意事项:实际项目中整数类型、切片、 map 的元素数量等都可以用 int 来表示。在涉及 到二进制传输、为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
var num int64
num = 123
fmt.Printf("值:%v 类型%T", num, num)
}

unsafe.Sizeof

unsafe.Sizeof(n1) 是 unsafe 包的一个函数,可以返回 n1 变量占用的字节数

1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"unsafe"
)
func main() {
var a int8 = 120
fmt.Printf("%T\n", a)
fmt.Println(unsafe.Sizeof(a))
}

3.int 不同长度直接的转换

1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
)
func main() {
var num1 int8
num1 = 127
num2 := int32(num1)
fmt.Printf("值:%v 类型%T", num2, num2) //值:127 类型 int32
}

4.数字字面量语法

Go1.13 版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮 点数的格式定义数字,例如:

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。

v := 0o377,代表八进制的 377,相当于十进制的 255。

v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。

而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 等于 123456。

三. 浮点型

Go 语言支持两种浮点型数:float32 和 float64。这两种浮点型数据格式遵循 IEEE754 标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。float64 的 浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用 fmt 包配合动词%f,代码如下:

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) //默认保留 6 位小数
fmt.Printf("%.2f\n", math.Pi) //保留 2 位小数
}

1.Go 语言中浮点数默认是 float64

1
2
num := 1.1
fmt.Printf("值:%v--类型:%T", num, num) //值:1.1--类型:float64

2.Golang 中 float 精度丢失问题

几乎所有的编程语言都有精度丢失这个问题,这是典型的二进制浮点数精度损失问题,在定 长条件下,二进制小数和十进制小数互转可能有精度丢失

1
2
3
4
5
6
7
8
9
d := 1129.6
fmt.Println((d * 100)) //输出:112959.99999999999

var d float64 = 1129.6
fmt.Println((d * 100)) //输出:112959.99999999999

m1 := 8.2
m2 := 3.8
fmt.Println(m1 - m2) // 期望是 4.4,结果打印出了 4.399999999999999

可以使用第三方包来解决精度丢失问题

3.golang科学计数法表示浮点类型

1
2
3
4
num8 := 5.1234e2 // ? 5.1234 * 10 的 2 次方
num9 := 5.1234E2 // ? 5.1234 * 10 的 2 次方 shift+alt+向下的箭头
num10 := 5.1234E-2 // ? 5.1234 / 10 的 2 次方 0.051234
fmt.Println("num8=", num8, "num9=", num9, "num10=", num10)

四. 布尔值

Go 语言中以 bool 类型进行声明布尔型数据,布尔型数据只有 true(真)和 false(假)两个 值

注意:

  1. 布尔类型变量的默认值为 false
  2. Go 语言中不允许将整型强制转换为布尔型.
  3. 布尔型无法参与数值运算,也无法与其他类型进行转换
1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"unsafe"
)
func main() {
var b = true
fmt.Println(b, "占用字节:", unsafe.Sizeof(b))
}

五. 字符串

Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、 float32、float64 等)一样。 Go 语言里的字符串的内部实现使用 UTF-8 编码。 字符串的值 为双引号(“”)中的内容,可以在 Go 语言的源码中直接添加非 ASCII 码字符,例如

1
2
s1 := "hello"
s2 := "你好"

1.字符串转义符

转义符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\‘ 单引号
\“ 双引号
\\ 反斜杠

举个例子,我们要打印一个 Windows 平台下的一个文件路径:

1
2
3
4
5
6
7
package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Code\\demo\\go.exe\"")
}

2.多行字符串

Go 语言中要定义一个多行字符串时,就必须使用反引号字符:

1
2
3
4
5
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出

3.字符串常用操作

image-20221031144955466

len(str)求字符串的长度

1
2
var str = "this is str"
fmt.Println(len(str))

拼接字符串

1
2
3
4
var str1 = "你好" var str2 = "golang"
fmt.Println(str1 + str2)
var str3 = fmt.Sprintf("%v %v", str1, str2)
fmt.Println(str3)

strings.Split 分割字符串

1
2
3
var str = "123-456-789"
var arr = strings.Split(str, "-") //分割出来是个字符数组
fmt.Println(arr)

拼接字符串

1
2
3
var str = "this is golang" 
var flag = strings.Contains(str, "golang")
fmt.Println(flag)

判断首字符尾字母是否包含指定字符

1
2
3
4
var str = "this is golang" var flag = strings.HasPrefix(str, "this")
fmt.Println(flag)
var str = "this is golang" var flag = strings.HasSuffix(str, "go")
fmt.Println(flag)

判断字符串出现的位置

1
2
3
4
var str = "this is golang" var index = strings.Index(str, "is") //从前往后
fmt.Println(index)
var str = "this is golang" var index = strings.LastIndex(str, "is") //从后网前
fmt.Println(index)

Join 拼接字符串

1
2
3
var str = "123-456-789" var arr = strings.Split(str, "-")
var str2 = strings.Join(arr, "*")
fmt.Println(str2)

六. byte 和 rune 类型

组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符。 字符用单引号(’) 包裹起来,如:

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main() {
a := 'a'
b := '0' //当我们直接输出 byte(字符)的时候输出的是这个字符对应的码值
fmt.Println(a)
fmt.Println(b)
//如果我们要输出这个字符,需要格式化输出
fmt.Printf("%c--%c", a, b) //%c 相应 Unicode 码点所表示的字符
}

字节(byte):是计算机中 数据处理 的基本单位,习惯上用大写 B 来表示,1B(byte,字节) = 8bit(位

字符:是指计算机中使用的字母、数字、字和符号

一个汉字占用 3 个字节 一个字母占用一个字节

1
2
3
4
a := "m"
fmt.Println(len(a)) //1
b := "张"
fmt.Println(len(b)) //3
Go 语言的字符有uint8和rune两种:
  1. uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  2. rune 类型,代表一个 UTF-8 字符。

当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可 以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 遍历字符串
package main
import "fmt"
func main() {
s := "hello 张三"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}

输出: 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 229(å) 188(¼) 160( ) 228(ä) 184(¸) 137() 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 24352(张) 19977(三)

因为 UTF8 编码下一个中文汉字由 3 个字节组成,所以我们不能简单的按照字节去遍历一个 包含中文的字符串,否则就会出现上面输出中第一行的结果。 字符串底层是一个 byte 数组,所以可以和[]byte 类型相互转换。字符串是不能修改的 字符 串是由 byte 字节组成,所以字符串的长度是 byte 字节的长度。 rune 类型用来表示 utf8 字 符,一个 rune 字符由一个或多个 byte 组成。

rune 类型实际是一个 int32
1
2
c3 := "营" c4 := '营' 
fmt.Printf("C3 的类型%T--C4 的类型%T", c3, c4) //C3 的类型 string--C4 的类型 int32

七. 修改字符串

要修改字符串,需要先将其转换成[]rune 或[]byte,完成后再转换为 string。无论哪种转换, 都会重新分配内存,并复制字节数组

1
2
3
4
5
6
7
8
9
10
11
12
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1)) //pig

s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2)) //红萝卜
}

数据类型转换

Go 语言中只有强制类型转换,没有隐式类型转换

数值类型之间的相互转换

数值类型包括:整形和浮点型

整形转换

1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
var a int8 = 20
var b int16 = 40
var c = int16(a) + b //要转换成相同类型才能运行
fmt.Printf("值:%v--类型%T", c, c) //值:60--类型 int16
}

整形,浮点型转换

1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
var a float32 = 3.2
var b int16 = 6
var c = a + float32(b
fmt.Printf("值:%v--类型%T", c, c) //值:9.2--类型 float32
}

转换的时候建议从低位转换成高位,高位转换成低位的时候如果转换不成功就会溢出,和我 们想的结果不一样比如:

1
2
3
4
5
6
package main
func main() {
var a int16 = 129
var b = int8(a) // 范围 -128 到 127
println("b=", b) //b= -127 //错误
}

比如计算直角三角形的斜边长时使用 math 包的 Sqrt()函数,该函数接收的是 float64 类型的 参数,而变量 a 和 b 都是 int 类型的,这个时候就需要将 a 和 b 强制类型转换为 float64 类型

1
2
3
4
5
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数是 float64 类型,需要强制转换
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)

其他类型转换为String类型

sprintf 把其他类型转换成 string 类型

注意:sprintf 使用中需要注意转换的格式

int 为%d

float 为%f

bool 为%t

byte 为%c

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
package main
import "fmt"
func main() {
var i int = 20
var f float64 = 12.456
var t bool = true
var b byte = 'a' var strs string
var strs string
strs = fmt.Sprintf("%d", i)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%f", f)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%t", t)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
strs = fmt.Sprintf("%c", b)
fmt.Printf("str type %T ,strs=%v \n", strs, strs)
}

/*
输出:
d:\golang\src\demo01>go run main.go
str type string ,strs=20
str type string ,strs=12.456000
str type string ,strs=true
str type string ,strs=a
*/

使用 strconv 包里面的几种转换方法进行转换

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
package main
import ( "fmt"
"strconv"
)
func main() {
//1、int 转换成 string
var num1 int = 20
s1 := strconv.Itoa(num1)
fmt.Printf("str type %T ,strs=%v \n", s1, s1)
// 2、float 转 string
var num2 float64 = 20.113123
/* 参数 1:要转换的值
参数 2:格式化类型
'f'(-ddd.dddd)、
'b'(-ddddp±ddd,指数为二进制)、
'e'(-d.dddde±dd,十进制指数)、
'E'(-d.ddddE±dd,十进制指数)、
'g'(指数很大时用'e'格式,否则'f'格式)、
'G'(指数很大时用'E'格式,否则'f'格式)。
参数 3: 保留的小数点 -1(不对小数点格式化)
参数 4:格式化的类型
*/
s2 := strconv.FormatFloat(num2, 'f', 2, 64)
fmt.Printf("str type %T ,strs=%v \n", s2, s2)
// 3、bool 转 string
s3 := strconv.FormatBool(true)
fmt.Printf("str type %T ,strs=%v \n", s3, s3)
//4、int64 转 string
var num3 int64 = 20
/*第二个参数为 进制
*/
s4 := strconv.FormatInt(num3, 10)
fmt.Printf("类型 %T ,strs=%v \n", s4, s4)
}

String 类型转换成数值类型

以下均使用strconv包

string 类型转换成 int 类型

1
2
3
var s = "1234"
i64, _ := strconv.ParseInt(s, 10, 64)
fmt.Printf("值:%v 类型:%T", i64, i64)

string 类型转换成 float 类型

1
2
3
4
str := "3.1415926535" v1, _ := strconv.ParseFloat(str, 32)
v2, _ := strconv.ParseFloat(str, 64)
fmt.Printf("值:%v 类型:%T\n", v1, v1)
fmt.Printf("值:%v 类型:%T", v2,

string 类型转换成 bool 类型(意义不大)

1
2
b, _ := strconv.ParseBool("true") // string 转 bool
fmt.Printf("值:%v 类型:%T", b, b)

string 转字符

1
2
3
4
5
s := "hello 张三"
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()

数值类型没法和 bool 类型进行转换

注意:在 go 语言中数值类型没法直接转换成 bool 类型 bool 类型也没法直接转换成数值类型


运算符

算数运算符

image-20221031151716334

注意: ++(自增)和–(自减)在 Go 语言中是单独的语句,并不是运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
)
func main() {
fmt.Println("10+3=", 10+3) // =13
fmt.Println("10-3=", 10-3) // =7
fmt.Println("10*3=", 10*3) // =30
//除法注意:如果运算的数都是整数,那么除后,去掉小数部分,保留整数部分
fmt.Println("10/3=", 10/3) //3
fmt.Println("10.0/3=", 10.0/3) //3.3333333333333335
// 取余注意 余数=被除数-(被除数/除数)*除数
fmt.Println("10%3=", 10%3) // =1
fmt.Println("-10%3=", -10%3) // -1
fmt.Println("10%-3=", 10%-3) // =1
fmt.Println("-10%-3=", -10%-3) // =-1
}

**注意: **在 golang 中,++ 和 – 只能独立使用 错误写法如下:

1
2
3
4
var i int = 8
var a int
a = i++ //错误,i++只能独立使用
a = i-- //错误, i--只能独立使用

**注意: **在 golang 中没有前++ 错误写法如下

1
2
3
4
var i int = 1
++i // 错误,在 golang 没有 前++
--i // 错误,在 golang 没有 前--
fmt.Println("i=", i

++ –正确写法:

1
2
3
var i int = 1
i++
fmt.Println("i=", i)

关系运算符

image-20221031152017959

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import ( "fmt"
)
func main() {
//演示关系运算符的使用
var n1 int = 9
var n2 int = 8
fmt.Println(n1 == n2) //false
fmt.Println(n1 != n2) //true
fmt.Println(n1 > n2) //true
fmt.Println(n1 >= n2) //true
fmt.Println(n1 < n2) //flase
fmt.Println(n1 <= n2) //flase
flag := n1 > n2
fmt.Println("flag=", flag)
}

逻辑运算符

image-20221031152105215

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
package main
import ( "fmt"
)
func main() {
//演示逻辑运算符的使用 &&
var age int = 40
if age > 30 && age < 50 {
fmt.Println("ok1")
}
if age > 30 && age < 40 {
fmt.Println("ok2")
}
//演示逻辑运算符的使用 ||
if age > 30 || age < 50 {
fmt.Println("ok3")
}
if age > 30 || age < 40 {
fmt.Println("ok4")
}
//演示逻辑运算符的使用 !
if age > 30 {
fmt.Println("ok5")
}
if !(age > 30) {
fmt.Println("ok6")
}
}

逻辑运算符短路演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
)
func test() bool {
fmt.Println("test...")
return true
}
func main() {
var i int = 10
if i < 9 && test() {
fmt.Println("ok...")
}
if i > 9 || test() {
fmt.Println("hello...")
}
}

赋值运算符

image-20221031152315238

crispcookie: 这章不是有手就行


选择循环

if else(分支结构)

1.if条件判断基本写法

Go 语言中 if 条件判断的格式如下:

1
2
3
4
5
6
7
if 表达式 1 {
分支 1
} else if 表达式 2 {
分支 2
} else{
分支 3
}

当表达式 1 的结果为 true 时,执行分支 1,否则判断表达式 2,如果满足则执行分支2,都 不满足时,则执行分支3, if 判断中的 else if 和 else 都是可选的,可以根据实际需要进行选择

注意: Go 语言规定与 if 匹配的左括号{必须与 if 和表达式放在同一行,{放在其他位置会触发编译错误。 同理,与 else 匹配的{也必须与 else 写在同一行,else 也必须与上一个 if 或 else if 右边的大括号在同一行。

例如:

1
2
3
4
5
6
7
8
9
10
func ifDemo1() {
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}

if 条件判断特殊写法

if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,举个例子:

1
2
3
4
5
6
7
8
if score := 56; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}

for循环

Go 语言中的所有循环类型均可以使用 for 关键字来完成。

for 循环的基本格式:

for 初始语句;条件表达式;结束语句{

​ 循环体语句

}

条件表达式返回 true 时循环体不停地进行循环,直到条件表达式返回 false 时自动退出循环。

1
2
3
for i := 0; i < 10; i++ {
fmt.Println(i)
}

for 循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:

1
2
3
4
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}

for 循环的初始语句和结束语句都可以省略,例如:

1
2
3
4
5
i := 0
for i < 10 {
fmt.Println(i)
i++
}

注意: Go 语言中是没有 while 语句的,我们可以通过 for 代替

for 无限循环

for {

​ 循环体语句

}

for 循环可以通过 break、goto、return、panic 语句强制退出循环。

1
2
3
4
5
6
7
8
9
10
k := 1
for { // 这里也等价 for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break //break 就是跳出这个 for 循环
}
k++
}

for range(键值循环)

Go 语言中可以使用 for range 遍历数组、切片、字符串、map 及通道(channel)。 通过 for range 遍历的返回值有以下规律

  1. 数组、切片、字符串返回索引和值
  2. map 返回键和值
  3. 通道(channel)只返回通道内的值
1
2
3
4
5
6
7
8
9
str := "abc 上海"
for index, val := range str {
fmt.Printf("index=%d, val=%c \n", index, val)
}
str := "abc 上海"
for _, val := range str {
fmt.Printf("val=%c \n", val)
}

switch case

使用 switch 语句可方便的对大量的值进行条件判断

Go 语言规定每个 switch 只能有一个 default 分支

switch语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
break
case ".css":
fmt.Println("text/css")
break
case ".js":
fmt.Println("text/javascript")
break
default:
fmt.Println("格式错误")
break
}

Go 语言中每个 case 语句中可以不写 break,不加 break 也不会出现穿透的现象 如下例子

1
2
3
4
5
6
7
8
9
10
11
12
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
case ".css":
fmt.Println("text/css")
case ".js":
fmt.Println("text/javascript")
default:
fmt.Println("格式错误")
}

一个分支可以有多个值,多个 case 值中间使用英文逗号分隔

1
2
3
4
5
6
7
8
9
n := 2
switch n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}

另一种写法:

1
2
3
4
5
6
7
8
9
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}

注意: 上面两种写法的作用域

分支还可以使用表达式,这时候 switch 语句后面不需要再跟判断变量。例如:

1
2
3
4
5
6
7
8
9
10
11
age := 56
switch {
case age < 25:
fmt.Println("好好学习吧!")
case age > 25 && age <= 60:
fmt.Println("好好工作吧!")
case age > 60:
fmt.Println("好好享受吧!")
default:
fmt.Println("活着真好!")
}

2. switch 的穿透 fallthrought

fallthrough语法可以执行满足条件的 case 的下一个 case,是为了兼容 C 语言中的 case 设计 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func switchDemo5() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}

/*
输出:
a
b
*/

fallthrough默认只能穿透一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var num int = 10
switch num {
case 10:
fmt.Println("ok1")
fallthrough //默认只能穿透一层
case 20:
fmt.Println("ok2")
fallthrough
case 30:
fmt.Println("ok3")
default:
fmt.Println("没有匹配到..")
}

/*
输出:
ok1
ok2
ok3
*/

五. break(跳出循环)

Go 语言中 break 语句用于以下几个方面

  • 用于循环语句中跳出循环,并开始执行循环之后的语句。
  • break 在 switch(开关语句)中在执行一条 case
  • 在多重循环中,可以用标号 label 标出想 break 的循环。

1. switch(开关语句)中在执行一条 case后跳出语句的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extname := ".a"
switch extname {
case ".html":
fmt.Println("text/html")
break
case ".css":
fmt.Println("text/css")
break
case ".js":
fmt.Println("text/javascript")
break
default:
fmt.Println("格式错误")
break
}

2. for 循环中默认 break 只能跳出一层循环

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
for i := 0; i < 2;
for j := 0; j < 10; j++ {
if j == 2 {
break
}
fmt.Println("i j 的值", i, "-", j)
}
}
}
1
2
3
4
5
6
7
8
9
k := 1
for { // 这里也等价 for ; ; {
if k <= 10 {
fmt.Println("ok~~", k)
} else {
break //break 就是跳出这个 for 循环
}
k++
}

3. 在多重循环中,可以用标号 label 标出想 break 的循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func main() {
lable2:
for i := 0; i < 2; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break lable2
}
fmt.Println("i j 的值", i, "-", j)
}
}
}

continue(继续下次循环)

continue 语句可以结束当前循环,开始下一次的循环迭代过程

仅限在 for 循环内使用

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"
func main() {
for i := 0; i < 2; i++ {
for j := 0; j < 4; j++ {
if j == 2 {
continue
}
fmt.Println("i j 的值", i, "-", j)
}
}
}

/*
输出:
i j 的值 0 - 0
i j 的值 0 - 1
i j 的值 0 - 3
i j 的值 1 - 0
i j 的值 1 - 1
i j 的值 1 - 3
*/

在 continue 语句后添加here标签时,表示开始标签对应的循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import "fmt"
func main() {
here:
for i := 0; i < 2; i++ {
for j := 0; j < 4; j++ {
if j == 2 {
continue here
}
fmt.Println("i j 的值", i, "-", j)
}
}
}

/*
输出:
i j 的值 0 - 0
i j 的值 0 - 1
i j 的值 1 - 0
i j 的值 1 - 1
*/

goto(跳转到指定标签)

goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退 出上有一定的帮助。Go 语言中使用 goto 语句能简化一些代码的实现过程

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
package main
import "fmt"
func main() {
var n int = 30
fmt.Println("ok1")
if n > 20 {
goto label1
}
fmt.Println("ok2")
fmt.Println("ok3")
fmt.Println("ok4")
label1:
fmt.Println("ok5")
fmt.Println("ok6")
fmt.Println("ok7")
}


/*
输出:
ok1
ok5
ok6
ok7
*/


数组

Array(数组)的介绍

​ 数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素 (element),这种类型可以是任意的原始类型,比如 int、string 等,也可以是用户自定义的 类型。

​ 一个数组包含的元素个数被称为数组的长度。在 Golang 中数组是一个长度固定的数 据类型,数组的长度是类型的一部分,也就是说 [5]int 和 [10]int 是两个不同的类型

​ Golang 中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地 址中的,因而索引数组元素的速度非常快。

​ 和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活,但是想要理解 slice 工作原理的话需要先理解数组

image-20221031172637954

数组的基本用法:

1
2
3
4
5
6
7
// 定义一个长度为 3 元素类型为 int 的数组 a
var a [3]int
// 定义一个长度为 3 元素类型为 int 的数组 b 并赋值
var b [3]int
b[0] = 80
b[1] = 100
b[2] = 96

数组定义

1
var 数组变量名 [元素数量]T

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。**[5]int 和[4]int 是不同的类型**

1
2
3
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时 a 和 b 是不同的类型

数组可以通过下标进行访问,下标是从 0 开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会 panic(异常)。

数组的初始化

使用初始化列表

1
2
3
4
5
6
7
8
func main() {
var testArray [3]int //数组会初始化为 int 类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2 0]
fmt.Println(cityArray) //[北京 上海 深圳]
}

根据初始值的个数自行推断数组的长度(不指明数组长度)

1
2
3
4
5
6
7
8
9
10
func main() {
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(testArray) //[0 0 0]
fmt.Println(numArray) //[1 2]
fmt.Printf("type of numArray:%T\n", numArray) //type of numArray:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

指定索引值的方式来初始化数组

1
2
3
4
5
func main() {
a := [...]int{1: 1, 3: 5}
fmt.Println(a) // [0 1 0 5]
fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}

数组的遍历

for循环遍历

1
2
3
4
5
6
7
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法 1:for 循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
}

for range遍历

1
2
3
4
5
6
func main() {
var a = [...]string{"北京", "上海", "深圳"}
for index, value := range a {
fmt.Println(index, value)
}
}

数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func modifyArray(x [3]int) {
x[0] = 100
}
func modifyArray2(x [3][2]int) {
x[2][0] = 100
}
func main() {
a := [3]int{10, 20, 30}
modifyArray(a) //在 modify 中修改的是 a 的副本 x
fmt.Println(a) //[10 20 30]
b := [3][2]int{{1, 1}, {1, 1}, {1, 1},}
modifyArray2(b) //在 modify 中修改的是 b 的副本 x
fmt.Println(b) //[[1 1] [1 1] [1 1]]
}

注意:

  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的
  2. [n]*T 表示指针数组,[n]*T 表示数组指针

多维数组

Go 语言是支持多维数组的,这里以二维数组为例

1
2
var 数组变量名 [元素数量][元素数量]
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type

image-20221031173803737

二维数组的定义

1
2
3
4
5
6
7
func main() {
a := [3][2]string{
{"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"},
}
fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) //支持索引取值:重庆
}

二位数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
a := [3][2]string{
{"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}

/*
输出:
北京 上海
广州 深圳
成都 重庆
*/

注意: 多维数组只有第一层可以使用…来让编译器推导数组长度。例如

1
2
3
4
5
//支持的写法
a := [...][2]string{
{"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, }
//不支持多维数组的内层使用... b := [3][...]string{
{"北京", "上海"}, {"广州", "深圳"}, {"成都", "重庆"}, }

就是说只有第一个[]内能写…来推导长度


切片

Slice(切片)的介绍

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
func arraySum(x [4]int) int {
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}

func main() {
a := [4]int{1, 2, 3, 4}
println(arraySum(a))
b := [5]int{1, 2, 3, 4, 5}
println(arraySum(b)) //错误
}

这个求和函数只能接受[4]int 类型,其他的都不支持。所以传入长度为 5 的数组的时候就会报错

切片的定义

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含地址长度容量。 声明切片类型的基本语法如下:

1
2
var 切片变量名 []切片中的元素类型
var name []T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func main() {
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
fmt.Println(c == d) //切片是引用类型,不支持直接比较,只能和 nil 比较
}

关于nil

当你声明了一个变量 , 但却还并没有赋值时 , golang 中会自动给你的变量赋值一个默认零 值。这是每种类型对应的零值, 而nil则是指针,切片,map,channel,函数,接口的零值

1
2
3
4
5
6
7
8
bool -> false
numbers -> 0
string-> "" pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

切片的循环遍历

切片的遍历和数组的遍历是一样的,同样有两种方法

for循环遍历

1
2
3
4
5
6
var a = []string{"北京", "上海", "深圳"}
// 方法 1:for 循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

for range遍历

1
2
3
4
5
var a = []string{"北京", "上海", "深圳"}
// 方法 2:for range 遍历
for index, value := range a {
fmt.Println(index, value)
}

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组定义切片(很类似python)

定义切片

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// 基于数组定义切片
a := [5]int{55, 56, 57, 58, 59}
b := a[1:4] //基于数组 a 创建切片,包括元素 a[1],a[2],a[3]
fmt.Println(b) //[56 57 58]
fmt.Printf("type of b:%T\n", b) //type of b:[]int
}
还支持如下方式:
c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57 58]
e := a[:] //[55 56 57 58 59]

定义切片的切片

除了基于数组得到切片,我们还可以通过切片来得到切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
//切片再切片
a := [...]string{"北京", "上海", "广州", "深圳", "成都", "重庆"}
fmt.Printf("a:%v type:%T len:%d cap:%d\n", a, a, len(a), cap(a))
b := a[1:3]
fmt.Printf("b:%v type:%T len:%d cap:%d\n", b, b, len(b), cap(b))
c := b[1:5]
fmt.Printf("c:%v type:%T len:%d cap:%d\n", c, c, len(c), cap(c))
}
/*
输出:
a:[北京 上海 广州 深圳 成都 重庆] type:[6]string len:6 cap:6
b:[上海 广州] type:[]string len:2 cap:5
c:[广州 深圳 成都 重庆] type:[]string len:4 cap:4
*/

**注意:**对切片进行再切片时,索引不能超过原数组的长度,否则会出现索引越界的错误

关于切片的长度len和容量cap

切片拥有自己的长度和容量,我们可以通过使用内置的 len()函数求长度,使用内置的 cap() 函数求切片的容量

  • 长度: 就是它所包含的元素个数

  • 容量: 是从底层数组它的第一个元素开始数,到其底层数组元素末尾的个数

  • 切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s := []int{2, 3, 5, 7, 11, 13}
fmt.Println(s)
fmt.Printf("长度:%v 容量 %v\n", len(s), cap(s))
c := s[:2]
fmt.Println(c)
fmt.Printf("长度:%v 容量 %v\n", len(c), cap(c))
d := s[1:3]
fmt.Println(d)
fmt.Printf("长度:%v 容量 %v", len(d),cap(d))

/*
输出
[2 3 5 7 11 13]
长度:6 容量 6
[2 3]
长度:2 容量 6
[3 5]
长度:2 容量 5
*/

上面案例中

第一个printf: 底层数组的长度和容量, 均为6

image-20221031193612861

第二个printf: c:=s[:2]后输出:[2 3], 左指针 s[0],右指针 s[2] , 所以长度为 2,容量为 6

image-20221031193740855

第三个printf: d := s[1:3]后输出:[3 5], 左指针 s[1],右指针 s[3] , 所以长度为 2,容量为 5

image-20221031193947980

切片的本质

切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针切片的长度(len) 和切片的容量(cap)

举个例子,现在有一个数组 a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片 s1 := a[:5],相应示意图如下

image-20221031194043056

切片 s2 := a[3:6],相应示意图如下

image-20221031194115848

使用make()函数构造切片

上面都是基于数组来创建的切片,如果需要动态的创建一个切片我们就需要使用内置的make()函数,格式如下:

1
make([]T, size, cap)

其中:

  1. T:切片的元素类型
  2. size:切片中元素的数量
  3. cap:切片的容量

举个例子

1
2
3
4
5
6
7
func main() {
//定义一个元素为int类型, 长度为2, 容量为10的切片
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}

上面代码中 a 的内部存储空间已经分配了 10 个,但实际上只用了2个, 容量并不会影响当前元素的个数,所以 len(a)返回 2cap(a)则返回该切片的容量

切片不能直接用于比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。

切片唯一合法的比较操作是和 nil 比较。 一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是 0。

但是我们不能说一个长度和容量都是 0 的切片一定是nil, 例如下面的示例

1
2
3
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用 len(s) == 0 来判断,不应该使用 s == nil 来判断

切片是引用数据类型

切片是引用数据类型, 拷贝前后两个变量共享底层数组对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

1
2
3
4
5
6
7
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将 s1 直接赋值给 s2,s1 和 s2 共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}

切片的一些操作

append()方法为切片添加元素

Go 语言的内建函数 append()可以为切片动态添加元素,每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在 append()函数调用时,所以我们通常都需要用原变量接收 append 函数的返回值(返回值是经过append操作后的原素组)

给切片追加元素的错误写法:

1
2
3
4
s3 := []int{1, 2, 3, 5, 6, 7}
s3[6] = 8
fmt.Println(s3) //index out of range [6] with length 6
//会发生越界

append()方法为切片追加元素:

1
2
3
4
5
6
7
8
9
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlic
e), numSlice)
}
}

上面案例的printf输出:

1
2
3
4
5
6
7
8
9
10
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000

从上面的结果可以看出:

  • append()函数将元素追加到切片的最后并返回该切片
  • 切片 numSlice 的容量按照 1,2,4,8,16 这样的规则自动进行扩容,每次扩容后都是 扩容前的 2 倍
  • 切片的扩容策略:
    • 首先判断,如果新申请容量(cap)大于 2 倍的旧容量(old.cap),最终容量(newcap) 就是新申请的容量(cap)
    • 否则判断,如果旧切片的长度小于 1024,则最终容量(newcap)就是旧容量(old.cap)的两 倍,即(newcap=doublecap)
    • 否则判断,如果旧切片长度大于等于 1024,则最终容量(newcap)从旧容量(old.cap) 开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量 (newcap)大于等于新申请的容量(cap),即(newcap >= cap)
    • 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
    • 需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如 int 和 string 类型的处理方式就不一样

append()函数支持一次性追加多个元素

1
2
3
4
5
6
7
8
9
var citySlice []string
// 追加一个元素
citySlice = append(citySlice, "北京")
// 追加多个元素
citySlice = append(citySlice, "上海", "广州", "深圳")
// 追加切片
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]

append()函数支持追加(多个)切片

当追加切片时, 最后一个追加的元素要加**…**

1
2
3
4
s1 := []int{100, 200, 300}
s2 := []int{400, 500, 600}
s3 := append(s1, s2...)
fmt.Println(s3) //[100 200 300 400 500 600]

使用copy()函数赋值切片

先来看一个问题

1
2
3
4
5
6
7
8
9
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}

由于切片是引用类型,所以 a 和 b 其实都指向了同一块内存地址。修改 b 的同时 a 的值也会发生变化

Go 语言内建的 copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy() 函数的使用格式如下

1
copy(destSlice, srcSlice []T)

其中:

  1. srcSlice: 数据来源切片

  2. destSlice: 目标切片

  3. T: 切片中的元素类型

代码示例

1
2
3
4
5
6
7
8
9
10
11
func main() {
// copy()复制切片
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a) //使用 copy()函数将切片 a 中的元素复制到切片 c
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}

切片删除元素

Go 语言中

并没有

删除切片元素的专用方法(这是否…),我们可以使用切片本身的”特性”来删除元素。 代码如下

1
2
3
4
5
6
7
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为 2 的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}

总结一下就是:要从切片 a 中删除索引为 index 的元素,操作方法是 a = append(a[:index], a[index+1:]…)


Map

Map(集合)的介绍

map 是一种无序的基于 key-value 的数据结构,Go 语言中的 map 是引用类型,必须初始化才能使用。 Go 语言中 map 的定义语法如下

1
map[KeyType]ValueType

其中:

  1. KeyType:表示键的类型
  2. ValueType:表示键对应的值的类型

map 类型的变量默认初始值为 nil,需要使用 make()函数来分配内存。语法为

1
make(map[KeyType]ValueType, [cap])

其中cap表示map的容量, 该参数不是必须的

注意: :获取 map 的容量不能使用 cap, cap 返回的是数组切片分配的空间大小, 根本不能用于 map。要获取 map 的容量,可以用 len 函数

具体定义过程示例:

1
scoreMap := make(map[string]int)	//定义一个键类型为string,值类型为int的map

map的基本使用

map 中的数据都是成对出现的,map 的基本使用示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}

/*
输出
map[小明:100 张三:90]
100
type of a:map[string]int
*/

map也支持在声明时填充元素, 例如:

1
2
3
4
5
6
7
func main() {
userInfo := map[string]string{
"username": "crispcookie",
"password": "123456",
}
fmt.Println(userInfo)
}

判断某个键是否存在

Go 语言中有个判断 map 中键是否存在的特殊写法(“写法”,这也太随性了),格式如下:

1
value, ok := map 对象[key]

示例:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 9
scoreMap["小明"] = 100
// 如果 key 存在 ok 为 true,v 为对应的值;不存在 ok 为 false,v 为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}

map的遍历

Go 语言中使用 for range 遍历 map

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["李四"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}

但我们只想遍历 key 的时候,可以按下面的写法

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}

注意: 遍历 map 时的元素顺序与添加键值对的顺序无关(C++的底层时红黑树,按照键来进行排序,本人暂时还没研究go里的元素顺序是声明规则)

使用delete()函数删除键值对

使用 delete()内建函数从 map 中删除一组键值对,delete()函数的格式如下:

1
delete(map 对象, key)

其中:

  1. map 对象:表示要删除键值对的 map 对象
  2. key:表示要删除的键值对的键

示例:

1
2
3
4
5
6
7
8
9
10
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["李四"] = 60
delete(scoreMap, "小明")//将小明:100 从 map 中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}

元素为map类型的切片

下面的代码演示了切片中的元素为 map 类型时的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的 map 元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子" mapSlice[0]["password"] = "123456" mapSlice[0]["address"] = "海淀区"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}

值为切片类型的map

下面的代码演示了 map 中值为切片类型的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}


函数

函数的定义

函数是组织好的、可重复使用的、用于执行指定任务的代码块。本节介绍了 Go 语言中函数的相关内容

Go 语言中支持:函数匿名函数闭包 Go 语言中定义函数使用 func 关键字,具体格式如下:

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

其中:

  1. 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)
  2. 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔
  3. 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹, 并用,分隔
  4. 函数体:实现指定功能的代码块

示例:

1
2
3
func intSum(x int, y int) int {
return x + y
}

函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数

1
2
3
func sayHello() {
fmt.Println("Hello crispcookie")
}

函数的调用

定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下

1
2
3
4
5
func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}

注意,调用有返回值的函数时,可以不接收其返回值

函数参数

类型简写

函数的参数中如果相邻变量的类型相同,则可以省略类型,例如

1
2
3
func intSum(x, y int) int {
return x + y
}

上面的代码中,intSum 函数有两个参数,这两个参数的类型均为 int,因此可以省略 x 的类型,因为 y 后面有类型说明,x 参数也是该类型

可变参数

可变参数是指函数的参数数量不固定。Go 语言中的可变参数通过在参数名后加…来标识

注意: 可变参数通常要作为函数的最后一个参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func intSum2(x ...int) int {
fmt.Println(x) //x 是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}

func main(){
//调用函数
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
}

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func intSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}

func main(){
//调用函数
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 16
}

本质上,函数的可变参数是通过切片来实现的

函数返回值

Go 语言中通过 return 关键字向外输出返回值

函数多返回值

Go 语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来

示例:

1
2
3
4
5
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}

返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过 return 关键字返回

示例:

1
2
3
4
5
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}

函数变量作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量.

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
//定义全局变量 num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("num=%d\n", num) //函数中可以访问全局变量 num
}

func main(){
testGlobalVar() //num=10
}

局部变量

局部变量是函数内部定义的变量, 函数内定义的变量无法在该函数外使用

函数内定义的变量无法在该函数外使用

1
2
3
4
5
6
7
8
9
func testLocalVar() {
//定义一个函数局部变量 x,仅在该函数内生效
var x int64 = 100
fmt.Printf("x=%d\n", x)
}
func main() {
testLocalVar()
fmt.Println(x) // 此时无法使用变量 x
}

如果局部变量和全局变量重名,优先访问局部变量

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
//定义全局变量 num
var num int64 = 10
func testNum() {
num := 100
fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
testNum() // num=100
}

变量只作用于当前的{}内,在子{}中可以使用,通常我们会在 if 条件判断、for 循环、switch 语句 上使用这种定义变量的方式

1
2
3
4
5
6
7
8
func testLocalVar2(x, y int) {
fmt.Println(x, y) //函数的参数也是只在本函数中生效
if x > 0 {
z := 100 //变量 z 只在 if 语句块生效
fmt.Println(z)
}
//fmt.Println(z)//此处无法使用变量 z
}

函数类型与变量

定义函数类型

我们可以使用 type 关键字来定义一个函数类型,具体格式如下:

1
type calculation func(int, int) int

上面语句定义了一个 calculation 类型,它是一种函数类型,这种函数接收两个 int 类型的参 数并且返回一个 int 类型的返回值。 简单来说,凡是满足这个条件的函数都是 calculation 类型的函数,例如下面的 add 和 sub 是 calculation 类型

1
2
3
4
5
6
7
func add(x, y int) int {
return x + y
}

func sub(x, y int) int {
return x - y
}

add 和 sub 都能赋值给 calculation 类型的变量

1
2
3
var c calculation
c=add
c=sub

函数类型变量

我们可以声明函数类型的变量并且为该变量赋值, 让函数作为变量值

1
2
3
4
5
6
7
8
9
func main() {
var c calculation // 声明一个 calculation 类型的变量 c
c = add // 把 add 赋值给 c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用 add 一样调用 c
f := add // 将函数 add 赋值给变量 f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用 add 一样调用 f
}

高阶函数

高阶函数分为函数作为参数函数作为返回值两部分

函数可以作为参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
func add(x, y int) int {
return x + y
}

func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}

func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}

让人两眼一黑两眼一亮的语法

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
import (
"fmt"
)
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}

func do(s string) func(int, int) int {
switch s {
case "+":
return add
case "-":
return sub
default:
return nil
}
}
func main() {
var a = do("+")
fmt.Println(a(10, 20))
}

匿名函数和闭包

匿名函数

函数当然还可以作为返回值,但是在 Go 语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数, 就是没有函数名的函数,匿名函数的定义格式如下

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

匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
// 将匿名函数保存到变量
add := func(x, y)int{
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数

//自执行函数:匿名函数定义完加()直接执行, 最下面的()内即为参数
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}

闭包

闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}

func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}

变量 f 是一个函数并且它引用了其外部作用域中的 x 变量,此时 f 就是一个闭包。 在 f 的生命周期内,变量 x 也一直有效

闭包进阶示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}

闭包进阶示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}

}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}

func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}

使用闭包要牢记: 闭包=函数+引用环境

defer语句

defer使用

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

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
/*
输出结果:
start
end
3
2
1
*/

由于 defer 语句延迟调用的特性,所以 defer 语句能非常方便的处理资源释放问题。比如: 资源清理、文件关闭、解锁及记录时间等

defer的执行时机

在 Go 语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和 RET 指令两 步。而 defer 语句执行的时机就在返回值赋值操作后,RET 指令执行前。具体如下图所示:

image-20221031212340483

defer使用案例

案例1:

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

func f1() int {
x := 5
defer func() {
x++
}()
return x
}

func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}

/*
输出:
5
6
5
5
*/

案例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}

/*
输出:
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
*/

内置函数

内置函数

image-20221031213219114

Go 语言中目前(Go1.12)是没有异常机制的,但是使用 panic/recover 模式来处理错误。 panic 可以在任何地方引发,但 recover 只有在 defer 调用的函数中有效。 首先来看一个例子:

panic/recover 的基本使用

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 funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}

/*
输出:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
.../code/func/main.go:12
main.main()
.../code/func/main.go:20 +0x9
*/

程序运行期间 funcB 中引发了 panic 导致程序崩溃,异常退出了。这个时候我们就可以通过 recover 将程序恢复回来,继续往后执行

改进方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了 panic 错误,可以通过 recover 恢复过来
if err != nil {
fmt.Println("recover in B")
}
}() //延迟执行的立即执行函数
panic("panic in B")
}

func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}

注意:

  1. recover()必须搭配 defer 使用
  2. defer 一定要在可能引发 panic 之前定义

defer /recover 实现异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fn2() {
defer func() {
err := recover()
if err != nil {
fmt.Println("抛出异常给管理员发送邮件")
fmt.Println(err)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}

defer /panic/recover 抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
}
return errors.New("读取文件错误")
}

func fn3() {
defer func() {
err := recover()
if err != nil {
fmt.Println("抛出异常给管理员发送邮件")
}
}()
var err = readFile("xxx.go")
if err != nil {
panic(err)
}
fmt.Println("继续执行")
}

func main() {
fn3()
}

后记: ….这个异常处理, 两眼一黑


指针

关于指针

指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另 一个变量的内存地址

image-20221031214405517

要搞明白 Go 语言中的指针需要先知道 3 个概念:指针地址、指针类型和指针取值

Go 语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和 *(根据地址取值)

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。

Go 语言中使用& 字符放在变量前面对变量进行取地址操作。

Go 语言中的值类型(int、float、bool、string、 array、struct)都有对应的指针类型,如:*int、*int64、*string 等

取变量指针的语法如下

1
ptr := &v // 比如 v 的类型为 T

其中:

  1. v : 代表被取地址的变量,类型为 T
  2. ptr : 用于接收地址的变量,ptr 的类型就为T,称做 T 的指针类型。代表指针。

示例:

1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
var a = 10
var b = &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc0000100a8
fmt.Printf("b:%v type:%T\n", b, b) // b:0xc0000100a8 type:*int
fmt.Println("取 b 的地址:", &b) // 0xc000006028
}

b := &a 的图示:

image-20221031214658017

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作, 也就是指针取值,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
a := 10
b := &a // 取变量 a 的地址,将地址保存到指针 b 中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针的值去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}

/*
输出:
type of b:*int
type of c:int
value of c:10
*/

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

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

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

指针传值示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}

func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}

new和make

先看一个例子

1
2
3
4
5
func main() {
var userinfo map[string]string
userinfo["username"] = "张三"
fmt.Println(userinfo)
}
1
2
3
4
5
func main() {
var a *int
*a = 100
fmt.Println(*a)
}

执行上面的代码会引发 panic,为什么呢?

在 Go 语言中对于引用类型的变量,我们在使用 的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。

而对于值类型 的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配 内存,就引出来今天的 new 和 make。 Go 语言中 new 和 make 是内建的两个函数,主要用来分配内存

new函数分配内存

new 是一个内置的函数,它的函数签名如下

1
func new(Type) *Type

其中:

  1. Type 表示类型,new 函数只接受一个参数,这个参数是一个类型
  2. *Type 表示类型指针,new 函数返回一个指向该类型内存地址的指针

实际开发中 new 函数不太常用

,使用 new 函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子
1
2
3
4
5
6
7
8
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}

本节开始的示例代码中 var a *int 只是声明了一个指针变量 a 但是没有初始化,

指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new 函数对 a 进行初始化之后就可以正常对其赋值了

1
2
3
4
5
6
7
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}

make函数分配内存

make 也是用于内存分配的,区别于 new,它只用于 slice、map 以及 channel 的内存创建, 而且它返回的类型就是这三个类型本身而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针,make 函数的函数签名如下:

1
func make(t Type, size ...IntegerType) Type

make 函数是无可替代的,在使用 slice、map 以及 channel 的时候,都需要使用 make 进 行初始化,然后才可以对它们进行操作。这个在前面中都有说明,关于 channel 在后续的章节详细说明。

本节开始的示例中 var b map[string]int 只是声明变量 b 是一个 map 类型的变量,需要像下面 的示例代码一样使用 make 函数进行初始化操作之后,才能对其进行键值对赋值

1
2
3
4
5
6
func main() {
var userinfo map[string]string
userinfo = make(map[string]string)
userinfo["username"] = "张三"
fmt.Println(userinfo)
}

make和new的区别

  1. 二者都是用来做内存分配的
  2. make 只用于 slice、map 以及 channel 的初始化,返回的还是这三个引用类型本身
  3. 而 new 用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

结构体

关于Golang结构体

Golang 中没有“类”的概念两眼一黑,Golang 中的结构体和其他语言中的类有点相似。和其他面向对 象语言中的类相比,Golang 中的结构体具有更高的扩展性和灵活性。

Golang 中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型就无法满足需求了,Golang 提供了一种 自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称 struct。 也就是我们可以通过 struct 来定义自己的类型了。

Golang type 关键词自定义类型和类型别名

Golang 中通过 type 关键词定义一个结构体

自定义类型

在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型, Go 语 言中可以使用 type 关键字来定义自定义类型

1
type myInt int

上面代码表示:将 myInt 定义为 int 类型,通过 type 关键字的定义,myInt 就是一种新的类型,它具有 int 的特性

类型别名

Golang1.9 版本以后添加的新功能

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像 一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人

1
type TypeAlias = Type

我们之前见过的 rune 和 byte 就是类型别名,他们的底层定义如下

1
2
type byte = uint8
type rune = int32

自定义类型和类型别名的区别

类型别名与自定义类型表面上看只有一个等号的差异,通过下面的这段代码来理解它们 之间的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
//类型定义
type newInt int
//类型别名
type myInt = int
func main() {
var a newInt
var b myInt
fmt.Printf("type of a:%T\n", a) //type of a:main.newInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}

结果显示 a 的类型是 main.newInt,表示 main 包下定义的 newInt 类型。b 的类型是 int 类型

结构体定义初始化的几种方法

结构体的定义

使用 type 和 struct 关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

其中:

  1. 类型名:表示自定义结构体的名称,在同一个包内不能重复
  2. 字段名:表示结构体字段名。结构体中的字段名必须唯一
  3. 字段类型:表示结构体字段的具体类型

举个例子,我们定义一个 person(人)结构体,代码如下:

1
2
3
4
5
type person struct {
name string
city string
age int8
}

同样类型的字段也可以写在一行,

1
2
3
4
type person struct {
name, city string
age int8
}

注意: 结构体首字母可以大写也可以小写,大写表示这个结构体是公有的,在其他的包里面 可以使用。小写表示这个结构体是私有的,只有这个包里面才能使用

结构体实例化(第一种方法)

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。 结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型

1
var 结构体实例 结构体类型

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
var p1 person
p1.name = "张三" p1.city = "北京" p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={张三 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"张三", city:"北京", age:18}
}

结构体实例化(第二种方法)

还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
var p2 = new(person)
p2.name = "张三" p2.age = 20
p2.city = "北京"
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"张三", city:"北京", age:20}
}

从打印的结果中我们可以看出 p2 是一个结构体指针

注意:在 Golang 中支持对结构体指针直接使用.来访问结构体的成员。p2.name = “张三” ,其实在底层是(*p2).name = “张三”

结构体实例化(第三种方法)

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "zhangsan" p3.age = 30
p3.city = "深圳"
(*p3).age = 40 //这样也是可以的
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"zhangsan", city:"深圳", age:30}
}

结构体实例化(第四种方式)

通过键值对初始化进行结构体实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
type person struct {
name string
city string
age int
}

func main() {
p4 := person{
name: "zhangsan",
city: "北京",
age: 18,
}
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"zhangsan", city:"北京", age:18}
}

结构体实例化(第五种方式)

结构体指针进行键值对初始化

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
p5 := &person{
name: "zhangsan", city: "上海", age: 28, }
fmt.Printf("p5=%#v\n", p5) //p5=&main.person{name:"zhangsan", city:"上海", age:28}
}

当某些字段没有初始值的时候,这个字段可以不写。此时,没有指定初始值的字段的值为零值

1
2
3
4
5
6
7
8
9
10
type person struct {
name string
city string
age int
}
func main() {
p6 := &person{
city: "北京", }
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"", city:"北京", age:0}
}

结构体实例化(第六种方式)

使用值的列表初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
type person struct {
name string
city string
age int
}
func main() {
// 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p7 := &person{ "zhangsan", "北京", 28
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"zhangsan", city:"北京", age:28}
}

使用这种格式初始化时,需要注意

1.必须初始化结构体的所有字段。

2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。

3.该方式不能和键值初始化方式混用

结构体方法和接收者

在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法 就是定义了接收者的函数。接收者的概念就类似于java中的 this

方法的定义格式如下

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

其中:

  1. 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小 写字母,而不是 self、this 之类的命名。例如,Person 类型的接收者变量应该命名为 p, Connector 类型的接收者变量应该命名为 c 等
  2. 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
  3. 方法名、参数列表、返回参数:具体格式与函数定义相同

示例1: 给结构体 Person 定义一个方法打印 Person 的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type Person struct {
name string
age int8
}
func (p Person) printInfo() {
fmt.Printf("姓名:%v 年龄:%v", p.name, p.age)
}
func main() {
p1 := Person{
name: "小王子", age: 25, }
p1.printInfo()
}

值类型的接收者

当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针 的任意成员变量,在方法结束后,修改都是有效的,这种方式就十分接近于其他语言中面向 对象中的 this

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 Person struct {
name string
age int
}
//值类型接受者
func (p Person) printInfo() {
fmt.Printf("姓名:%v 年龄:%v\n", p.name, p.age)
}
//指针类型接收者
func (p *Person) setInfo(name string, age int) {
p.name = name
p.age = age
}
func main() {
p1 := Person{
name: "小王子", age: 25, }
p1.printInfo()
p1.setInfo("张三", 20)
p1.printInfo()
}

给任意类型添加方法

在 Go 语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type myInt int

func (m myInt) SayHello() {
fmt.Println("Hello, 我是一个 int。")
}

func main() {
var m1 myInt
m1.SayHello() //Hello, 我是一个 int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}

注意: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法

结构体匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

1
2
3
4
5
6
7
8
9
10
//Person 结构体 Person 类型
type Person struct {
string
int
}
func main() {
p1 := Person{ "小王子", 18,}
fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18}
fmt.Println(p1.string, p1.int) //北京 18
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针

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"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "张三",
Gender: "男", Address: Address{
Province: "广东", City: "深圳", },
}
fmt.Printf("user1=%#v\n", user1)
//user1=main.User{Name:" 张 三 ", Gender:" 男 ", Address:main.Address{Province:"广东", City:"深圳"}}
}

关于嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体 的字段

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
package main
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "张三" user3.Gender = "男"
//如果直接赋值user3.CreateTime会报错
// user3.CreateTime = "2020" // ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2020" //指定 Address 结构体中的 CreateTime
user3.Email.CreateTime = "2021" //指定 Email 结构体中的 CreateTime
}

嵌套匿名结构体

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"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "张三"
user2.Gender = "男"
user2.Address.Province = "广东" //通过匿名结构体.字段名访问
user2.City = "深圳" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2)
//user2=main.User{Name:" 张 三 ", Gender:" 男 ", Address:main.Address{Province:"广东", City:"深圳"}}
}

注意:当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找

结构体的继承

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
package main
import "fmt"
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) run() {
fmt.Printf("%s 会运动!\n", a.name)
}

//Dog 狗
type Dog struct {
Age int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s 会汪汪汪~\n", d.name)
}

func main() {
d1 := &Dog{
Age: 4,
//注意嵌套的是结构体指针
Animal: &Animal{
name: "阿奇",
},
}
d1.wang() //乐乐会汪汪汪~ d1.run() //乐乐会动!
}

看到这继承我两眼一黑


JSON序列化

结构体与 JSON 序列

给前端提供 Api 接口数据,就需要涉及到结构体和 Json 之间的相互转换

Golang JSON 序列化是指把结构体数据转化成 JSON 格式的字符串

Golang JSON 的反序列化 是指把 JSON 数据转化成 Golang 中的结构体对象

Golang中的序列化和反序列化主要通过 “encoding/json” 包 中 的 json.Marshal() 和 json.Unmarshal()方法实现

json序列化(结构体对象->json)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import ( "encoding/json"
"fmt"
)
type Student struct {
ID int
Gender string
name string //私有属性不能被 json 包访问
Sno string
}

func main() {
var s1 = Student{
ID: 1, Gender: "男", name: "李四", Sno: "s0001", }
fmt.Printf("%#v\n", s1)
//main.Student{ID:1, Gender:"男", name:"李四", Sno:"s0001"}
var s, _ = json.Marshal(s1)
jsonStr := string(s)
fmt.Println(jsonStr)
//{"ID":1,"Gender":"男","Sno":"s0001"}
}

json反序列化(json->结构体对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import ( "encoding/json"
"fmt"
)
type Student struct {
ID int
Gender string
Name string
Sno string
}
func main() {
/* var jsonStr = "{\"ID\":1,\"Gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"}" */
var jsonStr = `{"ID":1,"Gender":"男","Name":"李四","Sno":"s0001"}`
var student Student
err := json.Unmarshal([]byte(jsonStr), &student)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 student=%#v student.Name=%v \n", student, student.Name)
//反序列化后 student=main.Student{ID:1, Gender:"男", Name:"李四", Sno:"s0001"} student.Name=李四
}

结构体标签tag

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag 在结构体字段的 后方定义,由一对反引号包裹起来,具体的格式如下:

1
`key1:"value1" key2:"value2"`

结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结 构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔

注意: 为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取 值。例如不要在 key 和 value 之间添加空格

序列化示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import ( "encoding/json"
"fmt"
)
type Student struct {
ID int `json:"id"` //通过指定 tag 实现 json 序列化该字段时的 key
Gender string `json:"gender"`
Name string
Sno string
}
func main() {
var s1 = Student{
ID: 1, Gender: "男", name: "李四", Sno: "s0001", }
fmt.Printf("%#v\n", s1)
var s, _ = json.Marshal(s1)
jsonStr := string(s)
fmt.Println(jsonStr)
}

/*
输出
main.Student{ID:1, Gender:"男", Name:"李四", Sno:"s0001"}
{"id":1,"gender":"男","Name":"李四","Sno":"s0001"}
*/

反序列化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
ID int `json:"id"` //通过指定 tag 实现 json 序列化该字段时的 key
Gender string `json:"gender"`
Name string
Sno string
}
func main() {
var s2 Student
var str = "{\"id\":1,\"gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"}"
err := json.Unmarshal([]byte(str), &s2)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%#v", s2)
//main.Student{ID:1, Gender:"男", Name:"李四", Sno:"s0001"}
}

五. 嵌套结构体和 JSON 序列化反序列化

序列化示例:

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
package main
import (
"encoding/json"
"fmt"
)
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []Student
}
func main() {
c := &Class{
Title: "001",
Students: make([]Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := Student{
Name: fmt.Sprintf("stu%02d", i), Gender: "男", ID: i, }
c.Students = append(c.Students, stu)
}
//JSON 序列化:结构体-->JSON 格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
}

/*
输出
json:{
"Title":"001",
"Students":[
{"ID":0,"Gender":"男","Name":"stu00"},
{"ID":1,"Gender":"男","Name":"stu01"},
{"ID":2,"Gender":"男","Name":"stu02"},
{"ID":3,"Gender":"男","Name":"stu03"},
{"ID":4,"Gender":"男","Name":"stu04"},
{"ID":5,"Gender":"男","Name":"stu05"},
{"ID":6,"Gender":"男","Name":"stu06"},
{"ID":7,"Gender":"男","Name":"stu07"},
{"ID":8,"Gender":"男","Name":"stu08"},
{"ID":9,"Gender":"男","Name":"stu09"}
]
}
*/

反序列化示例

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
package main
import (
"encoding/json"
"fmt"
)
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []Student
}
func main() {
str := `{
"Title":"001",
"Students":[
{"ID":0,"Gender":"男","Name":"stu00"},
{"ID":1,"Gender":"男","Name":"stu01"},
{"ID":2,"Gender":"男","Name":"stu02"},
{"ID":3,"Gender":"男","Name":"stu03"},
{"ID":4,"Gender":"男","Name":"stu04"},
{"ID":5,"Gender":"男","Name":"stu05"},
{"ID":6,"Gender":"男","Name":"stu06"},
{"ID":7,"Gender":"男","Name":"stu07"},
{"ID":8,"Gender":"男","Name":"stu08"},
{"ID":9,"Gender":"男","Name":"stu09"}
]
}`
c1 := &Class{}
err := json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}

/*
输出:
&main.Class{Title:"001", Students:[]main.Student{main.Student{ID:0, Gender:"男", Name:"stu00"}, main.Student{ID:1, Gender:"男", Name:"stu01"}, main.Student{ID:2, Gender:"男", Name:"
stu02"}, main.Student{ID:3, Gender:"男", Name:"stu03"}, main.Student{ID:4, Gender:"男", Name:"stu04"}, main.Student{ID:5, Gender:"男", Name:"stu05"}, main.Student{ID:6, Gender:"男",
Name:"stu06"}, main.Student{ID:7, Gender:"男", Name:"stu07"}, main.Student{ID:8, Gender:"男", Name:"stu08"}, main.Student{ID:9, Gender:"男", Name:"stu09"}}}

*/

Package包

Golang 中包的介绍和定义

包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 提供了很多内置包,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。

Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方

  • 系统内置包: Golang 语言给我们提供的内置包,引入后可以直接使用,如 fmt、strconv、strings、 sort、errors、time、encoding/json、os、io 等
  • 自定义包:开发者自己写的包
  • 第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面介绍的 “github.com/shopspring/decimal”包解决 float 精度丢失问题

Golang 包管理工具 go mod

在 Golang1.11 版本之前如果我们要自定义包的话必须把项目放在 GOPATH 目录。Go1.11 版 本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH 指定目录下,你可以在你磁盘的任何位置新建一个项目 , Go1.13 以后可以彻底不要 GOPATH 了1.

go mod init初始化项目

实际项目开发中我们首先要在我们项目目录中用 go mod 命令生成一个 go.mod 文件管理我 们项目的依赖。 比如我们的 golang 项目文件要放在了 crisp 这个文件夹,这个时候我们需要在 crisp 文件夹 里面使用 go mod 命令生成一个 go.mod 文件

使用go mod init 项目文件夹名字生成go.mod文件

image-20221101091740676

当然如果你和我一样比较懒, 你也可以下载Goland, 对着项目文件夹右键点击新建->Go模块文件

image-20221101091931649

也可以创建一样的go.mod文件

go mod 其他命令

使用 go mod [arguments] 执行以下go mod 命令

image-20221101092258910

Golang 中自定义包

包(package)是多个 Go 源码的集合,一个包可以简单理解为一个存放多个.go 文件的文件 夹。该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包

1
package 包名

注意:

  • 一个文件夹下面直接包含的文件只能归属一个 package,同样一个 package 的文件不能 在多个文件夹下
  • 包名可以不和文件夹的名字一样,包名不能包含 - 符号
  • 包名为 main 的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译 不包含 main 包的源代码则不会得到可执行文件

定义一个包

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识 符必须是对外可见的(public)。在 Go 语言中只需要将标识符的首字母大写就可以让标识 符对外可见了

定义一个包名为 calc 的包

1
2
3
4
5
6
7
8
9
10
11
package calc
//首字母大小表示公有,首字母小写表示私有
var a = 100 //私有变量
var Age = 20 //公有变量
func Add(x, y int) int {
return x + y
}
func Sum(x, y int) int {
return x - y
}

**2. main.go 中引入这个包 **

访问一个包里面的公有属性方法的时候需要通过包名称.去访问

1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"crisp/calc"
)
func main() {
c := calc.Add(10, 20)
fmt.Println(c)
}

导入一个包

单行导入

1
2
import "包 1"
import "包 2

多行导入

1
2
3
4
import ( 
"包 1"
"包 2"
)

匿名导入

如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下

1
import _ "包的路径"

匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中

自定义包名

在导入包名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。具体语法格式如下

1
import 别名 "包的路径"

Golang中使用第三方包

可以在 https://pkg.go.dev/ 查找看常见的 golang 第三方包

找到需要下载安装的第三方包的地址

以前面的的解决 float 精度损失的包 decimal 为例

https://github.com/shopspring/decimal

安装这个包

第一种方法: go get 包名称 (全局)

1
go get github.com/shopspring/decimal

第二种方法:go mod download (全局)

1
go mod download 

依赖包会自动下载到**$GOPATH/pkg/mod**,多个项目可以共享缓存的 mod,注意使用 go mod download 的时候首先需要在你的项目里面引入第三方包

第三种方法: go mod vendor 将依赖复制到当前项目的 vendor

1
go mod vendor

将依赖复制到当前项目的 vendor 下

注意: 使用 go mod vendor 的时候, $GOPATH/pkg/mod内已经引入了第三方包

3. 查阅文档使用包

ctrl c+v时间


接口

接口的介绍

Golang 中的接口是一种抽象数据类型,Golang 中接口定义了对象的行为规范,只定义规范 不实现。接口中定义的规范由具体的对象来实现

通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必 须得按照接口的规范

Golang接口的定义

​ 在 Golang 中接口(interface)是一种类型,一种抽象的类型。接口(interface)是一组函数method 的集合,Golang 中的接口不能包含任何变量

​ 在 Golang 中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想

​ Golang 中的接口也是一种数据类型不需要显示实现只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口

Golang 中每个接口由数个方法组成,接口的定义格式如下

1
2
3
4
type 接口名 interface{
方法名 1( 参数列表 1 ) 返回值列表 1
方法名 2( 参数列表 2 ) 返回值列表 2
}

其中:

  1. 接口名:使用 type 将接口定义为自定义的类型名。Go 语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。接口名最好要能突出该接口的类型含义
  2. 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被 接口所在的包(package)之外的代码访问
  3. 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略

示例: 定义一个 Usber 接口让 Phone 和 Camera 结构体实现这个接口

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 Usber interface {
Start()
Stop()
}
type Phone struct {
Name string
}
func (p Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p Phone) Stop() {
fmt.Println("phone 停止")
}

type Camera struct {
}
func (c Camera) Start() {
fmt.Println("相机 开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机 停止工作")
}
func main() {
phone := Phone{Name: "小米手机",}
var p Usber = phone //phone 实现了 Usb 接口
p.Start()
camera := Camera{}
var c Usber = camera //camera 实现了 Usb 接口
c.Start()
}

示例: Computer 结构体中的 Work 方法必须传入一个 Usb 的接口

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
package main
import "fmt"
type Usber interface {
Start()
Stop()
}

type Phone struct {
Name string
}
func (p Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p Phone) Stop() {
fmt.Println("phone 停止")
}

type Camera struct {
}
func (c Camera) Start() {
fmt.Println("相机 开始工作")
}
func (c Camera) Stop() {
fmt.Println("相机 停止工作")
}

//电脑的结构体
type Computer struct {
Name string
}
// 电脑的 Work 方法要求必须传入 Usb 接口类型数据
func (c Computer) Work(usb Usber) {
usb.Start()
usb.Stop()
}
func main() {
phone := Phone{Name: "小米手机", }
camera := Camera{}
computer := Computer{}
//把手机插入电脑的 Usb 接口开始工作
computer.Work(phone)
//把相机插入电脑的 Usb 接口开始工作
computer.Work(camera)
}

空接口

Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口

空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型(和JAVA的泛型差不多)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 定义一个空接口 x, x 变量可以接收任意的数据类型
var x interface{}
s := "你好 golang"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数

1
2
3
4
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}

map 的值实现空接口

使用空接口实现可以保存任意值的字典

1
2
3
4
5
6
// 空接口作为 map 值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "张三"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

切片实现空接口

1
2
var slice = []interface{}{"张三", 20, true, 32.2}
fmt.Println(slice)

类型断言

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值。 如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式

1
x.(T)

其中:

  1. x : 表示类型为 interface{}的变量
  2. T : 表示断言 x 可能是的类型

该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若 为 true 则表示断言成功,为 false 则表示断言失败

示例:

1
2
3
4
5
6
7
8
9
10
func main() {
var x interface{}
x = "Hello golnag" v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}

上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来 实现

注意: 类型.(type)只能结合 switch 语句使用

1
2
3
4
5
6
7
8
9
10
11
12
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}

因为空接口可以存储任意类型值的特点,所以空接口在 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
package main
import "fmt"
type Usb interface {
Start()
Stop()
}

type Phone struct {
Name string
}
func (p Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p Phone) Stop() {
fmt.Println("phone 停止")
}

func main() {
phone1 := Phone{Name: "小米手机", }
var p1 Usb = phone1 //phone1 实现了 Usb 接口 phone1 是 Phone 类型
p1.Start() //小米手机 开始工作
phone2 := &Phone{Name: "苹果手机", }
var p2 Usb = phone2 //phone2 实现了 Usb 接口 phone2 是 *Phone 类型
p2.Start() //苹果手机 开始工作
}

指针接受者

如果结构体中的方法是指针接收者,那么实例化后结构体指针类型都可以赋值给接口变量, 结构体值类型没法赋值给接口变量

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
package main
import "fmt"
type Usb interface {
Start()
Stop()
}

type Phone struct {
Name string
}
func (p *Phone) Start() {
fmt.Println(p.Name, "开始工作")
}
func (p *Phone) Stop() {
fmt.Println("phone 停止")
}

func main() {
/* 错误写法
phone1 := Phone{Name: "小米手机", }
var p1 Usb = phone1
p1.Start() */
//正确写法
phone2 := &Phone{Name: "苹果手机", }
var p2 Usb = phone2 //phone2 实现了 Usb 接口 phone2 是 *Phone 类型
p2.Start() //苹果手机 开始工作
}

一个结构体实现多个接口

Golang 中一个结构体也可以实现多个接口

示例:

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
package main
import "fmt"
type AInterface interface {
GetInfo() string
}
type BInterface interface {
SetInfo(string, int)
}

type People struct {
Name string
Age int
}
func (p People) GetInfo() string {
return fmt.Sprintf("姓名:%v 年龄:%d", p.Name, p.Age)
}
func (p *People) SetInfo(name string, age int) {
p.Name = name
p.Age = age
}

func main() {
var people = &People{Name: "张三", Age: 20, }
// people 实现了 AInterface 和 BInterface
var p1 AInterface = people
var p2 BInterface = people
fmt.Println(p1.GetInfo())
p2.SetInfo("李四", 30)
fmt.Println(p1.GetInfo())
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口

示例:

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
package main
import "fmt"
type SayInterface interface {
say()
}
type MoveInterface interface {
move()
}
// 接口嵌套
type Animal interface {
SayInterface
MoveInterface
}
type Cat struct {
name string
}
func (c Cat) say() {
fmt.Println("喵喵喵")
}
func (c Cat) move() {
fmt.Println("猫会动")
}
func main() {
var x Animal
x = Cat{name: "花花"}
x.move()
x.say()
}

多协程

进程、线程以及并行、并发

关于进程和线程

进程(Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基 本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进 程都有一个自己的地址空间。一个进程至少有 5 种基本状态,它们是:初始态,执行态, 等待状态,就绪状态,终止状态

通俗的讲进程就是一个正在执行的程序

线程 是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位

一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话 至少有一个进程

image-20221101113331402

关于并行和并发

并发:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。

并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行

通俗的讲多线程程序在单核 CPU 上面运行就是并发,多线程程序在多核 CUP 上运行就是并行,如果线程数大于 CPU 核数, 则多线程程序在多个 CPU 上面运行既有并行又有并发

单核cpu

image-20221101113504298

多核cpu

image-20221101113533583

Golang的协程(goroutine)

golang 中的主线程:(可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程 上可以起多个协程。Golang 中多协程可以实现并行或者并发

协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是 完全由用户自己的程序进行调度的。Golang 的一大特色就是从语言层面原生支持协程,在 函数或者方法前面加 go 关键字就可创建一个协程。可以说 Golang 中的协程就是 goroutine

image-20221101121407737

Golang 中的多协程类似其他语言中的多线程

多协程和多线程::Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。 OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协 程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。 这也是为什么越来越多的大公司使用 Golang 的原因之一

Goroutine 的使用以及 sync.WaitGroup

并行执行需求

在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 50 毫秒秒输出 “你好 golang” 在主线程中也每隔 50 毫秒输出”你好 golang”, 输出10次后,退出程序,要求主线程和 goroutine 同时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("tesst () hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println(" main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}

上面代码看上去没有问题,但是要注意主线程执行完毕后即使协程没有执行完毕,程序 会退出,所以我们需要对上面代码进行改造

image-20221101121836677

sync.WaitGroup 可以实现主线程等待协程执行完毕

示例:

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
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var wg sync.WaitGroup //1、定义全局的 WaitGroup

func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test () 你好 golang
time.Sleep(time.Millisecond * 50)
}
wg.Done() // 4、goroutine 结束就登记-1
}

func main() {
wg.Add(1) //2、启动一个 goroutine 就登记+1
go test()
for i := 1; i <= 2; i++ {
fmt.Println(" main() 你好 golang" + strconv.Itoa(i))
time.Sleep(time.Millisecond * 50)
}
wg.Wait() // 3、等待所有登记的 goroutine 都结束
}

Goroutine并发

Go 语言中实现并发非常简单,我们还可以启动多个 goroutine。

示例: (这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine 结束就登记-1
fmt.Println("Hello Goroutine!", i)
}

func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个 goroutine 就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的 goroutine 都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine 是并发执行的,而 goroutine 的调度是随机的

设置 Golang 并行运行的时候占用的 cup 数量

Go 运行时的调度器使用 GOMAXPROCS 参数来确定需要使用多少个 OS 线程来同时执行 Go 代码, 默认值是机器上的 CPU 核心数

例如在一个 8 核心的机器上,调度器会把 Go 代码同 时调度到 8 个 OS 线程上。 Go 语言中可以通过 runtime.GOMAXPROCS()函数设置当前程序并发时占用的 CPU 逻辑核心数

Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数

1
2
3
4
5
6
7
8
9
10
11
package main
import ( "fmt"
"runtime"
)
func main() {
//获取当前计算机上面的 Cup 个数
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum) //我允许出的是12
//可以自己设置使用多个 cpu
runtime.GOMAXPROCS(cpuNum - 1)
}

Goroutine并发案例

通过传统for循环来统计素数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
start := time.Now().Unix()
for num := 1; num <= 120000; num++ {
flag := true //假设是素数
for i := 2; i < num; i++ {
if num%i == 0 { //说明该 num 不是素数
flag = false
break
}
}
if flag {
// fmt.Println(num)
}
}
end := time.Now().Unix()
fmt.Println("普通的方法耗时=", end-start)
}

多个协程统计统计素数

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
package main
import ( "fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func fn1(n int) {
for num := (n-1)*30000 + 1; num <= n*30000; num++ {
flag := true //假设是素数
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
// fmt.Println(num)
}
}
wg.Done()
}
func main() {
start := time.Now().Unix()
for i := 1; i <= 4; i++ {
wg.Add(1)
go fn1(i)
}
wg.Wait()
end := time.Now().Unix()
fmt.Println("采用多协程的方法耗时=", end-start)
}

对比两种方法的执行的结果可以发现, goroutine 已经能大大的提升性能

如果我们想统计数据和打印数据同时进行,这个时候我们就可以使用管道(Channel)

Golang并发安全和锁

需求:现在要计算 1-60 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成

思路

  1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.
  2. 启动多个协程,将统计的将结果放入到 map 中

只使用 Goroutine 实现,运行的时候可能会出现资源争夺问题 concurrent map writes

示例:

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
package main
import (
"fmt"
"sync"
_"time"
)
var (
myMap = make(map[int]int)
wg sync.WaitGroup
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
wg.Done()
}
func main() {
for i := 1; i <= 60; i++ {
wg.Add(1)
go test(i)
}
wg.Wait()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源

Go 语言中使用 sync 包的 Mutex 类型来实现互斥锁。使用互斥锁来修复上面 代码的问题

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
package main
import (
"fmt"
"sync"
_"time"
)

var (
myMap = make(map[int]int)
wg sync.WaitGroup
lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
wg.Done()
}
func main() {
for i := 1; i <= 60; i++ {
wg.Add(1)
go test(i)
}
wg.Wait()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}

注意:

  • 使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等 锁;
  • 当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区
  • 多个 goroutine 同时等待一个锁时,唤醒的策略是随机的

虽然使用互斥锁能解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯, 并不利于多个协程对全局变量的读写操作。这个时候我们也可以通过另一种方式来实现上面的功能管道(Channel)

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资 源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。

读写锁在 Go 语言中使用 sync 包中的 RWMutex 类型。

读写锁分为两种:读锁写锁

当一个 goroutine 获取读锁之后,其他的 goroutine 如果是获 取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待

示例:

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
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时 10 毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}

func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时 1 毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}

需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来

Goroutine Recover 解决协程中出现的 Panic

示例:

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
package main
import (
"fmt"
"time"
)
//函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func test() {
//这里我们可以使用 defer + recover
defer func() {
//捕获 test 抛出的 panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个 map
var myMap map[int]string
myMap[0] = "golang" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}

Channel

Channel(管道)的介绍

​ 管道是 Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用 channel 在 多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制, 管道是协程之间的通信方式

​ Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

     Go 语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循**先入先出**(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型

Channel的定义

channel 是一种类型,一种引用类型。声明管道类型的格式如下

1
2
3
4
5
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的管道
var ch2 chan bool // 声明一个传递布尔型的管道
var ch3 chan []int // 声明一个传递 int 切片的管道

声明的管道后需要使用 make 函数初始化之后才能使用

1
make(chan 元素类型, 容量)

示例:

1
2
3
4
5
6
//创建一个能存储 10 个 int 类型数据的管道
ch1 := make(chan int, 10)
//创建一个能存储 4 个 bool 类型数据的管道
ch2 := make(chan bool, 4)
//创建一个能存储 3 个[]int 切片类型数据的管道
ch3 := = make(chan []int, 3)

channel相关操作

管道有发送(send)、接收(receive)和关闭(close)三种操作

发送和接收都使用**<-**符号

先使用以下语句定义一个管道

1
ch := make(chan int, 3)

发送(将数据放在管道内)

将一个值发送到管道中

1
ch <- 10 // 把 10 发送到 ch 中

接收(从管道内取值)

从一个管道中接收值

1
2
x := <- ch // 从 ch 中接收值并赋值给变量 x
<-ch // 从 ch 中接收值,忽略结果

关闭管道

通过调用内置的 close 函数来关闭管道

1
close(ch)

关于关闭管道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制(GC)回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的

关闭后的管道有以下特点:

  1. 对一个关闭的管道再发送值就会导致 panic
  2. 对一个关闭的管道进行接收会一直获取值直到管道为空
  3. 对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值
  4. 关闭一个已经关闭的管道会导致 panic。

管道阻塞

无缓冲的管道:

如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道, 无缓冲的管道又称为阻塞的管道

示例:

1
2
3
4
5
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/goroutine/main.go:10 +0x5b
exit status 2

有缓冲的管道:

解决上面问题的方法还有一种就是使用有缓冲区的管道。我们可以在使用 make 函数初始化管道的时候为其指定管道的容量

示例:

1
2
3
4
5
func main() {
ch := make(chan int, 1) // 创建一个容量为 1 的有缓冲区管道
ch <- 10
fmt.Println("发送成功")
}

只要管道的容量大于零,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。

管道阻塞示例:

1
2
3
4
5
6
func main() {
ch := make(chan int, 1)
ch <- 10
ch <- 12
fmt.Println("发送成功")
}

解决方法示例:

1
2
3
4
5
6
7
8
9
func main() {
ch := make(chan int, 1)
ch <- 10 //放进去
<-ch //取走
ch <- 12 //放进去
<-ch //取走
ch <- 17 //还可以放进去
fmt.Println("发送成功")
}

for range从管道循环取值

当向管道中发送完数据时,我们可以通过 close 函数来关闭管道。 当管道被关闭时,再往该管道发送值会引发 panic,从该管道取值的操作会先取完管道中的值,再然后取到的值一直都是对应类型的零值。

那如何判断一个管道是否被关闭了呢 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
//循环遍历管道数据
func main() {

var ch1 = make(chan int, 5)

for i := 0; i < 5; i++ {
ch1 <- i + 1
}
close(ch1) //关闭管道

/*使用 for range 遍历管道,当管道被关闭的时候就会退出 for range,如果没有关闭管道
就会报个错误 fatal error: all goroutines are asleep - deadlock!*/

//通过 for range 来遍历管道数据 管道没有 key
for val := range ch1 {
fmt.Println(val)
}
}

从上面的例子中可以看到有两种方式在接收值的时候判断该管道是否被关闭,不过通常情况使用的是 for range 的方式。使用 for range 遍历管道,当管道被关闭的时候就会退出 for range。

channel结合goroutine使用

示例1:

定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行

要求:

  1. 开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
  2. 开启一个 fn2 的协程读取 inChan 中写入的数据
  3. fn1 和 fn2 要同时操作一个管道
  4. 主线程必须等待操作完成后才可以退出
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
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func fn1(intChan chan int) {
for i := 0; i < 100; i++ {
//将数据写入管道
intChan <- i + 1
fmt.Println("writeData 写入数据-", i+1)
time.Sleep(time.Millisecond * 100)
}
close(intChan)
wg.Done()
}
func fn2(intChan chan int) {
for v := range intChan {
//从管道中读取数据
fmt.Printf("readData 读到数据=%v\n", v)
time.Sleep(time.Millisecond * 50)
}
wg.Done()
}
func main() {
allChan := make(chan int, 100)
wg.Add(1)
go fn1(allChan)
wg.Add(1)
go fn2(allChan)
wg.Wait()
fmt.Println("读取完毕...")
}

示例2

goroutine 结合 channel 实现统计 1-120000 的数字中那些是素数

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
73
74
75
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
//向 intChan 放入 1-120000 个数
func putNum(intChan chan int) {
for i := 1; i <= 1000; i++ {
intChan <- i
}
//关闭 intChan
close(intChan)
wg.Done()
}
// 从 intChan 取出数据,并判断是否为素数,如果是,就放入到 primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
for num := range intChan {
var flag bool = true
for i :=2; i < num; i++ {
if num%i == 0 { //说明该 num 不是素数
flag = false
break
}
}
if flag {
//将这个数就放入到 primeChan
primeChan <- num
}
}
//判断关闭
exitChan <- true
wg.Done()
}
//打印素数的方法
func printPrime(primeChan chan int) {
for v := range primeChan {
fmt.Println(v)
}
wg.Done()
}

func main() {
start := time.Now().Unix()
intChan := make(chan int, 1000)
primeChan := make(chan int, 20000) //放入结果
//标识退出的管道
exitChan := make(chan bool, 8) // 8 个
//开启一个协程,向 intChan 放入 1-8000 个数
wg.Add(1)
go putNum(intChan)
//开启 4 个协程,从 intChan 取出数据,并判断是否为素数,如果是,就放入到 primeChan
for i := 0; i < 8; i++ {
wg.Add(1)
go primeNum(intChan, primeChan, exitChan)
}
//打印素数
wg.Add(1)
go printPrime(primeChan)
//判断什么时候退出
wg.Add(1)
go func() {
for i := 0; i < 8; i++ {
<-exitChan
}
//当我们从 exitChan 取出了 8 个结果,就可以放心的关闭 prprimeChan
close(primeChan)
wg.Done()
}()
wg.Wait()
end := time.Now().Unix()
fmt.Println(end - start)
fmt.Println("main 线程退出")
}

单向管道

有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收

声明channel为只写

在声明的时候在chan右边加上指向管道的单箭头

1
2
var chanWriter chan<- int
chanWriter = make(chan int, 3)

声明channel为只读

在声明的时候在chan左边加上指出管道的单箭头

1
var chanReader <-chan int

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
//1. 在默认情况下下,管道是双向
//var chan1 chan int //可读可写
//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 20
//num := <-chan2 //error
fmt.Println("chan2=", chan2)
//3. 声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //err
fmt.Println("num2", num2)

select多路复用

传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock,在实际开发中,可能我们不好确定什么关闭该管道

你也许会写出如下代码使用遍历的方式来实现:

1
2
3
4
5
6
7
for{
// 尝试从 ch1 接收值
data, ok := <-ch1
// 尝试从 ch2 接收值
data, ok := <-ch2

}

这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作

select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对应一个管道的通信(接收或发送)过程。select 会一直等待,直到某个 case 的通信操作完成 时,就会执行 case 分支对应的语句。具体格式如下

1
2
3
4
5
6
7
8
9
10
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}

示例:

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
package main
import (
"fmt"
"time"
)
func main() {

/*使用 select 可以解决从管道取数据的阻塞问题*/

//1.定义一个管道 10 个数据 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}

//2.定义一个管道 5 个数据 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}

for {
select {
/*注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock,会自动到
下一个 case 匹配*/
case v := <-intChan:
fmt.Printf("从 intChan 读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从 stringChan 读取的数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到了\n")
time.Sleep(time.Second)
return
}
}
}

使用 select 语句能提高代码的可读性

  • 可处理一个或多个 channel 的发送/接收操作
  • 如果多个 case 同时满足,select 会随机选择一个
  • 对于没有 case 的 select{}会一直等待,可用于阻塞 main 函数

反射

反射的引入

有时我们需要写一个函数,这个函数有能力统一处理各种值类型,而这些类型可能无法共享同一个接口,也可能布局未知,也有可能这个类型在我们设计函数时还不存在,这个时候我们就可以用到反射

  • 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么? 值是什么呢?
    • 方法一: 使用类型断言 X.(T)
    • 方法二: 使用反射实现,也就是在程序运行时动态的获取一个变量的类型信息和值信息
  • 把结构体序列化成 json 字符串,自定义结构体 Tag 标签的时候就用到了反射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"encoding/json"
"fmt"
)
type Student struct{
ID int `json:"id"`
Gender string `json:"gender"`
Name string `json:"name"`
Sno string `json:"sno"`
}
func main() {
var s1 = Student{
ID: 1, Gender: "男", Name: "李四", Sno: "s0001", }
var s, _ = json.Marshal(s1)
jsonStr := string(s)
fmt.Println(jsonStr)
//{"id":1,"gender":"男","name":"李四","sno":"s0001"}
}

ORM框架也用到了反射技术, Mybatis就是半自动的ORM框架

ORM:对象关系映射(Object Relational Mapping,简称 ORM)是通过使用描述对象和数据库 之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中

反射的介绍

反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时, 变量被转换为内存地址, 变量名不会被编译器写入到可执行部分。在运行程序时, 程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、 结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运 行期获取类型的反射信息,并且有能力修改它们

Golang中反射可以实现的功能

  • 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型 类别
  • 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法、结构体的 tag
  • 通过反射,可以修改变量的值,可以调用关联的方法

Golang的变量分为两个部分

  • 类型信息:预先定义好的元信息
  • 值信息:程序运行过程中可动态变化的

在 GoLang 的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。

在 GoLang 中,反射的相关功能由内置的 reflect 包提供,任意接口值在反射中都可以理解为 由 reflect.Type 和 reflect.Value 两部分组成, 并且reflect包提供了reflect.TypeOfreflect.ValueOf 两个重要函数来获取任意对象的 Value 和 Type

reflect.TypeOf()获取任意值的类型对象

在 Go 语言中,使用 reflect.TypeOf()函数可以接收任意 interface{}参数,可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("type:%v\n", v)
}
func main() {
var a float32 = 12.5
reflectType(a) // type:float32
var b int64 = 100
reflectType(b) // type:int64
}

type Name 和 type Kind

在反射中关于类型还划分为两种:类型(Type)和种类(Kind)

因为在 Go 语言中我们可 以使用 type 关键字构造很多自定义类型,而种类(Kind)就是指底层的类型

Go 语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空

在反射中, 当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind). 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类

示例:

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
package main
import (
"fmt"
"reflect"
)

type myInt int64

type Person struct {
Name string
Age int
}

type Animal struct {
Name string
}

func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("TypeOf:%v Name:%v Kind:%v\n", t, t.Name(), t.Kind())
}

func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // TypeOf:*float32 Name: Kind:ptr
reflectType(b) // TypeOf:main.myInt Name:myInt Kind:int64
reflectType(c) // TypeOf:int32 Name:int32 Kind:int32
var d = Person{
Name: "crisp",
Age: 20,
}
var e = Animal{
Name: "小花",
}
reflectType(d) // TypeOf:main.Person Name:Person Kind:struct
reflectType(e) // TypeOf:main.Animal Name:Animal Kind:struct
var f = []int{1, 2, 3, 4, 5}
reflectType(f) //TypeOf:[]int Name: Kind:slice
}

在reflect包中定义的Kind类型如下

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 Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号 8 位整型
Int16 // 有符号 16 位整型
Int32 // 有符号 32 位整型
Int64 // 有符号 64 位整型
Uint // 无符号整型
Uint8 // 无符号 8 位整型
Uint16 // 无符号 16 位整型
Uint32 // 无符号 32 位整型
Uint64 // 无符号 64 位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64 位复数类型
Complex128 // 128 位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)

reflect.ValueOf()

reflect.ValueOf()返回的是 reflect.Value 类型,其中包含了原始值的值信息。reflect.Value 与原 始值之间可以互相转换

reflect.Value 类型提供的获取原始值的方法如下:

方法 转换后的类型 说明
Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均 可以此方式返回
Bool() bool 将值以 bool 类型返回
Bytes() []bytes 将值以字节数组 []bytes
String() string 将值以字符串类型返回

通过反射获取原始值示例 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
"reflect"
)
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
var c = v.Int() + 6 //获取反射的原始值
fmt.Println(c)
}
func main() {
var a int64 = 100
reflectValue(a)
}

通过反射获取原始值示例 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
package main
import (
"fmt"
"reflect"
)
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值
fmt.Printf("type is int64, value is %d\n", v.Int())
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值
fmt.Printf("type is float32, value is %f\n", v.Float())
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值
fmt.Printf("type is float64, value is %f\n", v.Float())
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将 int 类型的原始值转换为 reflect.Value 类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通过反射设置变量的值

image-20221101200336095

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝

必须传递变量地址才能修改变量值。而反射中使用专有的 Elem()方法来获取指针对应的值

示例:

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
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect 包会引发 panic
}
}

func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}

func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}

结构体反射

与结构体相关的方法

任意值通过 reflect.TypeOf()获得反射对象信息后

如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 **NumField()**和 Field()方法获得结构体成员的详细信息

reflect.Type 中与获取结构体成员相关的的方法

方法 返回值 说明
Field(i int) StructField 根据索引,返回索引对应的结构体字段的信息
NumField() int 返回结构体成员字段数量
FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息
FieldByIndex(index []int) StructField 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息
FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段
NumMethod() int 返回该类型的方法集中方法的数目
Method(int) Method 返回该类型方法集中的第 i 个方法
MethodByName(string) (Method,bool) 根据方法名返回该类型方法集中的方法

StructField 类型

StructField 类型用来描述结构体中的一个字段的信息。StructField 的定义如下:

1
2
3
4
5
6
7
8
9
type StructField struct {
// 参见 http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string // Name 是字段的名字
PkgPath string //PkgPath 是非导出字段的包路径,对导出字段该字段为"" Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于 Type.FieldByIndex 时的索引切片
Anonymous bool // 是否匿名字段
}

结构体反射示例

当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息

获取结构体属性,获取执行结构体方法

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
73
74
75
76
77
78
79
80
81
82
83
package main
import (
"fmt"
"reflect"
)
//student 结构体
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Score int `json:"score"`
}

func (s Student) GetInfo() string {
var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score)
fmt.Println(str)
return str
}

func (s *Student) SetInfo(name string, age int, score int) {
s.Name = name
s.Age = age
s.Score = score
}

func (s *Student) Print() {
fmt.Println("打印方法...")
}
//打印字段
func PrintStructField(s interface{}) {
t := reflect.TypeOf(s)
// v := reflect.ValueOf(s)
kind := t.Kind()
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("传入的不是结构体")
return
}
//1、通过类型变量里面的 Field 可以获取结构体的字段
field0 := t.Field(0)
fmt.Println(field0.Name)
fmt.Println(field0.Type)
fmt.Println(field0.Tag.Get("json"))
//2、通过类型变量里面的 FieldByName 可以获取结构体的字段
field1, _ := t.FieldByName("Age")
fmt.Println(field1.Name)
fmt.Println(field1.Type)
fmt.Println(field1.Tag.Get("json"))
//3、获取到该结构体有几个字段
num := t.NumField()
fmt.Println("字段数量:", num)
}
//方法
func PrintStructFn(s interface{}) {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("传入的不是结构体")
return
}
//1、通过类型变量里面的 Method 可以获取结构体的方法
var tMethod = t.Method(0) //注意
fmt.Println(tMethod.Name)
fmt.Println(tMethod.Type)
//2、通过类型变量获取这个结构体有多少个方法
fmt.Println(t.NumMethod())
//3、执行方法 (注意需要使用值变量,并且要注意参数)
// v.Method(0).Call(nil)
v.MethodByName("Print").Call(nil)
//4、执行方法传入参数 (注意需要使用值变量,并且要注意参数)
var params []reflect.Value //声明了 []reflect.Value
params = append(params, reflect.ValueOf("张三"))
params = append(params, reflect.ValueOf(22))
params = append(params, reflect.ValueOf(100))
v.MethodByName("SetInfo").Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Va
lue
// 5、执行方法获取方法的值
info := v.MethodByName("GetInfo").Call(nil)
fmt.Println(info)
}
func main() {
stu1 := Student{Name: "小明", Age: 15, Score: 98, }
// PrintStructField(stu1)
PrintStructFn(&stu1)
}

修改结构体方法

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
package main
import (
"fmt"
"reflect"
)
//student 结构体
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
Score int `json:"score"`
}

func (s Student) GetInfo() string {
var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score)
return str
}
//反射修改结构体属性
func reflectChangeStruct(s interface{}) {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Elem().Kind() != reflect.Struct {
fmt.Println("传入的不是结构体指针类型")
return
}
name := v.Elem().FieldByName("Name")
name.SetString("李四") // 设置值
age := v.Elem().FieldByName("Age")
age.SetInt(20) // 设置值
}

func main() {
stu1 := Student{
Name: "小明", Age: 15, Score: 98, }
// PrintStructField(stu1)
reflectChangeStruct(&stu1)
fmt.Println(stu1.GetInfo())
}


文件操作

打开和关闭文件

os.Open()函数能够打开一个文件,返回一个*File(文件指针) 和一个 err(错误信息)

这种方式打开文件是只读

操作完成文件对象以后一定要记得关闭文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
"os"
)
func main() {
// 只读方式打开当前目录下的 main.go 文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
fmt.Println(file) //&{0xc000078780}
defer file.Close() // 关闭文件
}

为了防止文件忘记关闭,我们通常使用 defer 注册文件关闭语句

file.Read()读取文件

Read 方法定义如下:

1
func (f *File) Read(b []byte) (n int, err error)

它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回 0 和 io.EOF

示例:

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
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 只读方式打开当前目录下的 main.go 文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
defer file.Close()
// 使用 Read 方法读取数据,注意一次只会读取 128 个字节
var tmp = make([]byte, 128)
n, err := file.Read(tmp)
if err == io.EOF {
fmt.Println("文件读完了")
return
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Printf("读取了%d 字节数据\n", n)
fmt.Println(string(tmp[:n]))
}

循环读取

使用 for 循环读取文件中的所有数据

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 main() {
// 只读方式打开当前目录下的 main.go 文件
file, err := os.Open("./main.go")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
defer file.Close()
// 循环读取文件
var content []byte
var tmp = make([]byte, 128)
for {
n, err := file.Read(tmp)
if err == io.EOF {
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
content = append(content, tmp[:n]...)
}
fmt.Println(string(content))
}


bufio读取文件

bufio 是在 file 的基础上封装了一层 API,支持更多的功能, 支持按行读取

buf有缓冲的意思(buffer)

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
package main
import (
"bufio"
"fmt"
"io"
"os"
)
// bufio 按行读取示例
func main() {
file, err := os.Open("C:/test.txt")
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') //注意是字符
if err == io.EOF {
if len(line) != 0 {
fmt.Println(line)
}
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Print(line)
}
}

ioutil读取整个文件

io/ioutil 包的 ReadFile 方法能够读取完整的文件,只需要将文件名作为参数传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
"io/ioutil"
)
// ioutil.ReadFile 读取整个文件
func main() {
content, err := ioutil.ReadFile("./main.go")
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Println(string(content))
}

文件写入操作

os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能

其中:

  1. name:要打开的文件名 flag:打开文件的模式
  2. perm:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01

模式有以下几种

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加

Write 和 WriteString

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.OpenFile("C:/test.txt", os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
str := "你好 golang"
file.Write([]byte(str)) //写入字节切片数据
file.WriteString("直接写入的字符串数据") //直接写入字符串数据
}

bufio.NewWriter

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.OpenFile("C:/test.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("open file failed, err:", err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("你好 golang\r\n") //将数据先写入缓存
}
writer.Flush() //将缓存中的内容写入文件(注意)
}

ioutil.WriteFile

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"io/ioutil"
)
func main() {
str := "hello golang"
err := ioutil.WriteFile("C:/test.txt", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
}

文件重命名

1
2
3
4
err := os.Rename("C:/test1.txt", "D:/test1.txt") //只能同盘操作
if err != nil {
fmt.Println(err)
}

复制文件

1. ioutil 进行复制

其实就是获取源文件的数据, 写入新的文件中

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
package main
import (
"fmt"
"io/ioutil"
)
//自己编写一个函数,接收两个文件路径 srcFileName dstFileName
func CopyFile(dstFileName string, srcFileName string) (err error) {
input, err := ioutil.ReadFile(srcFileName)
if err != nil {
fmt.Println(err)
return err
}
//ioutil.WriteFile往目标文件写入数据
err = ioutil.WriteFile(dstFileName, input, 0644)
if err != nil {
fmt.Println("Error creating", dstFileName)
fmt.Println(err)
return err
}
return nil
}
func main() {
srcFile := "c:/test1.zip"
dstFile := "D:/test1.zip"
err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Printf("拷贝完成\n")
} else {
fmt.Printf("拷贝错误 err=%v\n", err)
}
}

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
package main
import (
"fmt"
"io"
"os"
)
//自己编写一个函数,接收两个文件路径 srcFileName dstFileName
func CopyFile(dstFileName string, srcFileName string) (err error) {
source, _ := os.Open(srcFileName)
destination, _ := os.OpenFile(dstFileName, os.O_CREATE|os.O_WRONLY, 0666)
buf := make([]byte, 128)
for {
n, err := source.Read(buf)
if err != nil && err != io.EOF {
return err
}
if n == 0 {
break
}
if _, err := destination.Write(buf[:n]); err != nil {
return err
}
}
}
func main() {
//调用 CopyFile 完成文件拷贝
srcFile := "c:/000.avi" dstFile := "D:/000.avi" err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Printf("拷贝完成\n")
} else {
fmt.Printf("拷贝错误 err=%v\n", err)
}
}

创建目录

创建一个目录

1
2
3
4
err := os.Mkdir("./abc", 0666)
if err != nil {
fmt.Println(err)
}

创建多级目录

1
2
3
4
err := os.MkdirAll("dir1/dir2/dir3", 0666) //创建多级目录
if err != nil {
fmt.Println(err)
}

删除目录和文件

删除一个目录或文件

1
2
3
4
err := os.Remove("t.txt")
if err != nil {
fmt.Println(err)
}

一次删除多个目录或文件

1
2
3
4
err := os.RemoveAll("aaa")
if err != nil {
fmt.Println(err)
}