GO基础

Go语言基础

推荐教程 李文周的博客
Golang文档查询

开发环境搭建

环境安装

GO环境下载地址

查看安装环境

安装开发工具

VSCode下载

安装插件

windows配置代理,然后重启VsCode

1
2
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

ctrl + shift + p:输入Go Install/Update Tools

Hello World

新建一个文件 xx.go

1
2
3
func main() {
fmt.Println("Hello world")
}

在当前目录输入 go build xx.go,会生成一个可执行的文件
在当前目录输入 go run xx.go,会执行当前文件

基础

变量

声明变量:var 变量名 变量类型

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
// 单个申明变量
var name string
var age int
var isOk bool

// 批量声明
var (
name2 string // 默认值是 ""
age2 int // 默认值是 0
isOk2 bool // 默认值是 false
)

func main() {
// GO语言中变量声明必须要使用,不使用就无法编译通过
name2 = "小祥"
age2 = 10
isOk2 = true
// %s 占位符
fmt.Printf("name2:%s", name2)
fmt.Println(age2)
fmt.Println(isOk2)

// 非全局变量声明后必须使用
var a string = "a"
fmt.Println(a)
}

初始化多个变量

1
var name, age = "小祥", 10

类型推导

1
2
var name = "小祥"
var age = 22

短变量声明

1
2
3
//简短变量声明,只能在函数里用
b := 20
fmt.Println(b)

匿名变量

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

1
2
3
4
5
6
7
8
9
func foo() (int, string) {
return 20, "小祥"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}

注意事项

  1. 函数外的每个语句都必须以关键字开始(var、const、func等)
  2. :=不能使用在函数外。
  3. _多用于占位,表示忽略值。

常量

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 声明后就不能改变
const a = "a"

// 批量声明
const (
NUM1 = 1
NUM2 = 2
)

// 如果第一个声明后,后面没有赋值,后面的和上一个值一致
const (
B1 = 0
B2
B3
)

iota

iota是go语言的常量计数器,只能在常量的表达式中使用

iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用

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
// iota
const (
C1 = iota // 0
C2 = iota // 1
C3 // 2
C4 // 3
)
// 跳过某些值
const (
D1 = iota // 0
D2 // 1
_ // 2
D3 // 3
)
// 插队
const (
E1 = iota // 0
E2 = 10 // 10
E3 = iota // 2
E4 // 3
)
// 多个常量在一行
const (
F1, F2 = iota + 1, iota + 2 // F1: 1,F2: 2
F3, F4 = iota + 2, iota + 3 // F3: 3,F3: 4
)

基本数据类型

整型

整型分为以下两个大类:
按长度分为:int8、int16、int32、int64对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型

类型 描述
uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型

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

进制

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
i := 10
fmt.Printf("i: %d\n", i) // 十进制打印
fmt.Printf("i: %b\n", i) // 把十进制转换成二进制
fmt.Printf("i: %o\n", i) // 把十进制转换成八进制
fmt.Printf("i: %x\n", i) // 把十进制转换成十六进制

// 查看变量类型
fmt.Printf("i是 %T 类型\n", i) // int类型
i2 := int8(2)
fmt.Printf("i2是 %T 类型\n", i2) // int8类型
}

浮点数

1
2
3
4
5
6
7
func main() {
f1 := 1.203922
fmt.Printf("f1 是 %T 类型\n", f1) // 默认就是 float64类型

f2 := float32(1.223) // 声明 float32类型
fmt.Printf("f2 是 %T 类型\n", f2)
}

布尔值

默认是false

1
2
3
4
5
func main() {
b1 := true
fmt.Printf("b1 是 %T 类型\n", b1) // bool类型
}

字符串

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

多行字符串使用 `` 包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
b2 := "b2"
fmt.Printf("b2 是 %T 类型\n", b2) // string类型

// 多行字符串
b3 := `
第一行
第二行
第三行
`
fmt.Println(b3)

b4 := "b4"

fmt.Printf("字符串 %v\n", b4) // 字符串 b4
fmt.Printf("字符串 %#v\n", b4) // 字符串 "b4"
}
字符串转义符
转义符 描述
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\‘ 单引号
\“ 双引号
\\ 反斜杠
字符串常用操作
方法 描述
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split(str,par) 分割
strings.contains(str,par) 判断是否包含
strings.HasPrefix(str,par),strings.HasSuffix(str,par) 前缀\后缀判断
strings.Index(str,par),strings.LastIndex(str,par) 子串出现的位置
strings.Join(a[]string, sep string) join操作
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"
"strings"
)

func main() {
b4 := "b4"
// 查询字符串长度
fmt.Println(len(b4))

// 字符串拼接
b5 := "你好"
b6 := "字符串"
b7 := fmt.Sprintf("%s %s", b5, b6)
b8 := b5 + b6
fmt.Println("b7 = ", b7) // b7 = 你好 字符串
fmt.Println("b8 = ", b8) // b8 = 你好字符串

// 分割
b9 := "a-b-c-d-c"
fmt.Println(strings.Split(b9, "-")) // [a b c d]

// 判断是否包含
fmt.Println(strings.Contains(b9, "c")) // true

// 前缀 后缀判断
fmt.Println(strings.HasPrefix(b9, "a")) // true
fmt.Println(strings.HasSuffix(b9, "a")) // false

// 字符串出现的位置和最后一次出现的位置
fmt.Println(strings.Index(b9, "c")) // 4
fmt.Println(strings.LastIndex(b9, "c")) // 8

// 字符串join操作
fmt.Println(strings.Join(strings.Split(b9, "-"), ":")) // a:b:c:d:c
}
修改字符串

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

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

import (
"fmt"
"strings"
)

func main() {
b10 := "string"
// 强制类型转换
byteS1 := []byte(b10)
byteS1[0] = 'S'
fmt.Println(string(byteS1)) // String

b11 := "hot"
runeS2 := []rune(b11)
runeS2[0] = 'H'
fmt.Println(string(runeS2)) // Hot
}

流程控制

Go语言中最常用的流程控制有if和for
而switch和goto主要是为了简化代码、降低重复代码而生的结构,属于扩展类的流程控制

if else

1
2
3
4
5
6
7
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
age := 20
if age > 20 {
fmt.Println("年龄大于20")
} else {
fmt.Println("年龄不够20")
}

if age2 := 10; age2 > 10 {
fmt.Println("年龄大于等于10")
}

if age > 10 && age != 19 {
fmt.Println("年龄大于10 不等于19")
}
}

for

for循环可以通过break、continue(结束本次循环)、goto、return、panic语句强制退出循环。

1
2
3
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
func main() {
// 基本格式 0123456789
for i := 0; i < 10; i++ {
fmt.Printf("%d", i)
}
fmt.Println()
// 特殊格式1 56789
i := 5
for ; i < 10; i++ {
fmt.Printf("%d", i)
}
fmt.Println()
// 特殊格式2 3456789
var i2 int = 3
for i2 < 10 {
fmt.Printf("%d", i2)
i2++
}
// 死循环 for循环可以通过break、goto、return、panic语句强制退出循环。
for {
fmt.Println("死循环")
}
}

for range

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

  1. 数组、切片、字符串返回索引和值。
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
// range循环
/**
* 索引是 0,值是H
* 索引是 1,值是e
* 索引是 2,值是l
* 索引是 3,值是l
* 索引是 4,值是o
* 索引是 5,值是小
* 索引是 8,值是祥 一个中文字符占用3个字节
**/
s := "Hello小祥"
for i, v := range s {
fmt.Printf("索引是 %d,值是%c\n", i, v)
}
}
1
2
3
4
5
6
7
// 99乘法表
for i := 1; i < 10; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d * %d = %d ", i, j, i*j)
}
fmt.Println()
}

switch case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
f := 2
switch f {
case 1:
fmt.Println(1)
case 2, 3:
fmt.Println(2, 3)
case 4:
fmt.Println(4)
default:
fmt.Println("无")
}

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

数组

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化

1
var 数组变量名 [元素数量]T
1
2
3
4
5
6
// 数组长度是数组类型的一部分
func main() {
var a1 [3]int
var a2 [4]int
fmt.Printf("a1 是 %T, a2 是 %T\n", a1, a2) // a1 是 [3]int, a2 是 [4]int
}

数组初始化

如果不初始化,默认元素都是零值,布尔就是false、整型和浮点型是0、字符串是””

第一种方式

初始化数组时可以使用初始化列表来设置数组元素的值

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
func main() {
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
fmt.Println(numArray) //[1 2]
fmt.Printf("numArray是:%T\n", numArray) //numArray是:[2]int
fmt.Println(cityArray) //[北京 上海 深圳]
fmt.Printf("cityArray是:%T\n", cityArray) //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("类型a是:%T\n", a) //类型a是:[4]int
}

多维数组

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

数组是值传递

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

1
2
3
4
a2 := [2]int{1, 2}
a3 := a2
a3[0] = 100
fmt.Println(a2, a3) // [1,2] [100,2]

切片

1
var name []T

切片指向了一个底层的数组
切片的长度就是他元素的个数
切片的容量是底层数组从切片的第一个元素到最后一个元素的数量
切片是引用类型,真正的数据都存储在底层数组里的
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量
切片是一个框,框住的是一块连续的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
// 定义一个切片
var s1 []int
fmt.Println(s1) // 默认是空的
fmt.Println(s1 == nil) // true
s1 = []int{1, 2, 3}
fmt.Println(s1)
// 长度
fmt.Println(len(s1))
// 容量
fmt.Println(cap(s1))

s2 := s1[:2] // 相当于 s1[0,2]
s3 := s1[1:] // 相当于[1,len(s1)]
s4 := s1[:] // 相当于[0,len(s1)]
fmt.Println(s2, s3, s4) // [1 2] [2 3] [1 2 3]
s1[0] = 100 // 改变底层数组
fmt.Println(s2, s3, s4) // [100 2] [2 3] [100 2 3]

fmt.Printf("s2的长度为%d, 容量为%d\n", len(s2), cap(s2)) // s2的长度为2, 容量为3
fmt.Printf("s3的长度为%d, 容量为%d\n", len(s3), cap(s3)) // s3的长度为2, 容量为2
fmt.Printf("s4的长度为%d, 容量为%d\n", len(s4), cap(s4)) //s4的长度为3, 容量为3
}

切片的遍历

1
2
3
4
5
6
7
8
9
10
11
func main(){
d1 := make([]int, 3)

for i := 0; i < len(d1); i++ {
fmt.Println(d1[i])
}

for _, v := range d1 {
fmt.Println(v)
}
}

make函数

T: 切片的元素类型
size: 切片中元素的数量
cap: 切片的容量

1
make([]T, size, cap)
1
2
3
4
5
6
func main() {
a := make([]int, 2, 10)
fmt.Println(a) // [0,0]
fmt.Println(len(a)) // 2
fmt.Println(cap(a)) // 10
}

append方法

Go语言的内建函数append()可以为切片动态添加元素。
可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…
append()函数将元素追加到切片的最后并返回该切片
切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍

1
2
3
4
5
6
7
8
9
10
11
func main(){
var f1 []int // 不需要初始化,定义就可以使用append
f1 = append(f1, 1)
fmt.Println(f1) // [1]
// 追加多个元素
f1 = append(f1, 2, 3)
fmt.Println(f1) // [1 2 3]
f2 := []int{5, 6}
f1 = append(f1, f2...)
fmt.Println(f1) // [1 2 3 5 6]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
var h1 []int

for i := 0; i < 10; i++ {
h1 = append(h1, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", h1, len(h1), cap(h1), h1)
}
/**
打印
[0 1] len:2 cap:2 ptr:0xc000018100
[0 1 2] len:3 cap:4 ptr:0xc000016200
[0 1 2 3] len:4 cap:4 ptr:0xc000016200
[0 1 2 3 4] len:5 cap:8 ptr:0xc000012280
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc000012280
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc000012280
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc000012280
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc000122080
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc000122080
**/
}

copy函数

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

1
copy(destSlice, srcSlice []T)
1
2
3
4
5
6
7
8
9
10
func main() {
a := []int{1, 2, 3}
var b = make([]int, 3, 3)
copy(b, a)
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [1 2 3]
b[0] = 10
fmt.Println(a) // [1 2 3]
fmt.Println(b) //[10 2 3]
}

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素

  1. 切片不保存具体的值
  2. 切片对应一个底层数组
  3. 底层数组都是占用一块连续的内存
1
2
3
4
5
6
7
8
9
func main() {
// 从切片中删除元素
a := [...]int{1, 3, 5}
// 要删除索引为2的元素
a2 := a[:]
fmt.Println(a, a2) // [1 3 5] [1 3 5]
a2 = append(a2[:1], a2[2:]...) // 修改了底层数组
fmt.Println(a, a2) // [1 5 5] [1 5]
}

注意

要检查切片是否为空,使用len(s) == 0来判断,而不应该使用s == nil来判断。

1
2
3
4
5
6
7
8
func main() {
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
fmt.Println(len(s1), cap(s1), s1 == nil) // 0 0 true
fmt.Println(len(s2), cap(s2), s2 == nil) // 0 0 false
fmt.Println(len(s3), cap(s3), s3 == nil) // 0 0 false
}

拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容

1
2
3
4
5
6
7
8
func main() {
d1 := make([]int, 3)
d2 := d1
fmt.Println(d1) // [0 0 0]
d2[2] = 10
fmt.Println(d1) // [0 0 10]
fmt.Println(d2) // [0 0 10]
}

切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
  • 首先判断,如果新申请容量(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类型的处理方式就不一样。

map

基本结构

1
map[KeyType]ValueType
  • KeyType:表示键的类型。
  • ValueType:表示键对应的值的类型。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
map1 := make(map[int]string, 8) // 初始化一个map
fmt.Println(map1) // map[]
map1[1] = "1"
map1[2] = "2"
fmt.Println(map1) // map[1:1 2:2]
fmt.Println(map1[1]) // 1
map2 := map[string]bool{
"小米": true,
"大米": false,
} // 声明变量并初始化
fmt.Println(map2) // map[大米:false 小米:true]
// 判断值是否存在
_, ok := map2["1"]
fmt.Println("值为 ", ok) // 值为 false
}

使用delete()内建函数从map中删除一组键值对

1
2
3
4
5
6
7
8
9
10
func main() {
map3 := map[int]string{
1: "小明",
2: "小红",
3: "小白",
}
fmt.Println(map3) // map[1:小明 2:小红 3:小白]
delete(map3, 2)
fmt.Println(map3) // map[1:小明 3:小白]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
// 元素为map的切片
mS := make([]map[string]string, 2, 10)
for i, m := range mS {
fmt.Printf("索引:%d,值:%v\n", i, m)
}
// 索引:0,值:map[]
// 索引:1,值:map[]
map4 := map[string]string{
"1": "值1",
"2": "值2",
"3": "值3",
}
mS = append(mS, map4)
fmt.Println(mS) // [map[] map[] map[1:值1 2:值2 3:值3]]

// 值为切片的map
sM := make(map[string][]string, 2)
s := make([]string, 2, 4)
s[0] = "1"
s[1] = "2"
sM["1"] = s
fmt.Println(sM) // map[1:[1 2]]
}

指针

  • &:取地址
  • *:根据地址取值
1
2
3
4
5
6
7
8
9
func main() {
n := 10
p := &n
fmt.Println("指针地址为 ", p) // 指针地址为 0xc00000a0a8
fmt.Printf("类型为 %T\n", p) // 类型为 *int
m := *p
fmt.Println(m) // 10
fmt.Printf("类型为 %T\n", m) // 类型为 int
}

new和make

new

new是一个内置的函数

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

使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

1
2
3
4
5
6
7
8
9
10
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
*a = 100
fmt.Println(*a) // 100
}

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型

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

new与make的区别

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

函数

Go语言中定义函数使用func关键字

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

函数的调用

定义了函数之后,我们可以通过函数名()的方式调用函数

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

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

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

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

func intSum2(x int, y ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}

返回值

Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片

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

func calc2(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}

func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}

函数类型

我们可以使用type关键字来定义一个函数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值
// 凡是满足这个条件的函数都是c类型的函数
type c func(int, int) int

func add(x, y int) int {
return x + y
}

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

func main() {
var cc c
cc = add
}

高阶函数

函数可以作为参数

1
2
3
4
5
6
7
8
9
10
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
}

函数作为返回值

1
2
3
4
5
6
7
8
9
10
11
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}

匿名函数

匿名函数就是没有函数名的函数

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
}
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语句

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
*/
}

执行时机

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

面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

内置函数

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

类型别名和自定义类型

自定义类型

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

1
2
//将MyInt定义为int类型
type MyInt int

类型别名

1
2
3
4
type TypeAlias = Type

type byte = uint8
type rune = int32

两者区别

1
2
3
4
5
6
7
8
9
10
11
12
13
//类型定义
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。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

结构体

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

基本类型

Go语言中通过struct来实现面向对象
使用typestruct关键字来定义结构体

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

}
1
2
3
4
5
6
7
8
9
10
type person struct {
name string
city string
age int8
}

type person1 struct {
name, city string
age int8
}

实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

基本实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var 结构体实例 结构体类型


type person struct {
name string
city string
age int8
}

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}
}

匿名结构体

1
2
3
4
5
6
func main() {
var user struct{Name string; Age int}
user.Name = "小王子"
user.Age = 18
fmt.Printf("%#v\n", user)
}

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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
}

创建指针类型结构体

通过使用new关键字对结构体进行实例化,得到的是结构体的地址
Go语言中支持对结构体指针直接使用.来访问结构体的成员

1
2
3
4
5
6
7
8
func main() {
var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}
}

取结构体的地址实例化

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

1
2
3
4
5
6
7
8
9
func mian() {
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}
}

初始化

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值

1
2
3
4
5
6
7
8
func main() {
p5 := person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
}

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

1
2
3
4
5
6
7
8
func mian() {
p6 := &person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}
}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值

1
2
3
4
5
6
func main() {
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
}

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值
使用这种格式初始化时,需要注意:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。
1
2
3
4
5
6
7
8
func main() {
p8 := &person{
"沙河娜扎",
"北京",
28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"沙河娜扎", city:"北京", age:28}
}

结构体内存布局

结构体占用一块连续的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
}
// n.a 0xc0000a0060
// n.b 0xc0000a0061
// n.c 0xc0000a0062
// n.d 0xc0000a0063

构造函数

Go语言的结构体没有构造函数,我们可以自己实现
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type person struct {
name string
city string
age int8
}

func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

func main() {
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
}

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)

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

方法与函数的区别是:函数不属于任何类型,方法属于特定的类型

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Person 结构体
type Person struct {
name string
age int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}

//Dream Person做梦的方法
// 值类型接收者
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指针类型的接收者

// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}

func main() {
p1 := NewPerson("小王子", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}

嵌套结构体

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

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
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User2 struct {
Name string
Gender string
Address //匿名字段
Email
}


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:"威海"}}

var user2 User2
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:"威海"}}

var user3 User2
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

结构体中的继承

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
//Animal 动物
type Animal struct {
name string
}

func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}

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

结构体与JSON序列化

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

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
import (
"encoding/json"
"fmt"
)

type person struct {
Name string `json:"name" db:"name"`
Age int `json:"age"`
}

func main() {
p := person{
Name: "小祥",
Age: 20,
}
// 序列化
b, err := json.Marshal(p)
if err != nil {
fmt.Println("转换错误!")
return
}
fmt.Println(string(b))
// 反序列化
str := `{"name":"小祥","age":20,"a":1}`
var p2 person
_ = json.Unmarshal([]byte(str), &p2)
fmt.Println(p2)
}

接口

接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法

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

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

实现接口的条件

接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。

1
2
3
4
5
6
7
8
9
10
11
// Singer 接口
type Singer interface {
Sing()
}

type Bird struct {}

// Sing Bird类型的Sing方法,这样就称为Bird实现了Singer接口
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}

接口类型变量

1
2
3
4
5
6
7
var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say() // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say() // 汪汪汪

值接收者和指针接收者

使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量

1
2
3
4
// Mover 定义一个接口类型
type Mover interface {
Move()
}

值接收者实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Dog 狗结构体类型
type Dog struct{}

// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}

func main() {
var x Mover // 声明一个Mover类型的变量x

var d1 = Dog{} // d1是Dog类型
x = d1 // 可以将d1赋值给变量x
x.Move()

var d2 = &Dog{} // d2是Dog指针类型
x = d2 // 也可以将d2赋值给变量x
x.Move()
}

指针接收者实现接口

由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。
但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Cat 猫结构体类型
type Cat struct{}

// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
func main() {
var c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()

// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2 // 不能将c2当成Mover类型
}

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立

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
// Sayer 接口
type Sayer interface {
Say()
}

// Mover 接口
type Mover interface {
Move()
}
type Dog struct {
Name string
}

// 实现Sayer接口
func (d Dog) Say() {
fmt.Printf("%s会叫汪汪汪\n", d.Name)
}

// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
func main() {
var d = Dog{Name: "旺财"}

var s Sayer = d
var m Mover = d

s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法
}

多种类型实现同一接口

Go语言中不同的类型还可以实现同一接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Mover 接口
type Mover interface {
Move()
}
type Dog struct {
Name string
}
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}

// Car 汽车结构体类型
type Car struct {
Brand string
}

// Move Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
func main() {
var obj Mover

obj = Dog{Name: "旺财"}
obj.Move()

obj = Car{Brand: "宝马"}
obj.Move()
}

一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}

接口组合

接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例

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
// src/io/io.go

type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}

// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}

// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}

空接口

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。

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 "fmt"

// 空接口

// Any 不包含任何方法的空接口类型
type Any interface{}

// Dog 狗结构体
type Dog struct{}

func main() {
var x Any

x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}

通常我们在使用空接口类型时不必使用type关键字声明,可以像下面的代码一样直接使用interface{}

1
var x interface{}  // 声明一个空接口类型变量x

应用

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

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

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

1
2
3
4
5
6
7
8
9
func main() {
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

}

类型断言

想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下

1
x.(T)
  • x:表示接口类型的变量
  • T:表示断言x可能是的类型。
1
2
3
4
5
6
7
8
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "富贵" // 变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}

如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
// justifyType 对传入的空接口类型变量x进行类型断言
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语言中支持模块化的开发理念,在Go语言中使用包(package)来支持代码模块化和代码复用。
一个包是由一个或多个Go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、os、io等。

标识符可见性

如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。
在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。

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 demo

import "fmt"

type Student struct {
Name string // 可在包外访问的方法
class string // 仅限包内访问的字段
}

// 包级别标识符的可见性

// num 定义一个全局整型变量
// 首字母小写,对外不可见(只能在当前包内使用)
var num = 100

// Mode 定义一个常量
// 首字母大写,对外可见(可在其它包中使用)
const Mode = 1

// person 定义一个代表人的结构体
// 首字母小写,对外不可见(只能在当前包内使用)
type person struct {
name string
Age int
}

// Add 返回两个整数和的函数
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
return x + y
}

// sayHi 打招呼的函数
// 首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
var myName = "七米" // 函数局部变量,只能在当前函数内使用
fmt.Println(myName)
}

包的引入

要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方

1
import importname "path/to/package"
  • importname:引入的包名,通常都省略。默认值为引入包的包名。
  • path/to/package:引入包的路径名称,必须使用双引号包裹起来
  • Go语言中禁止循环导入包。
1
2
3
4
5
6
7
8
9
10
import "fmt"
import "net/http"
import "os"

// 或者使用
import (
"fmt"
"net/http"
"os"
)

如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入
一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。

1
import _ "github.com/go-sql-driver/mysql"

init初始化函数

在每一个Go源文件中,都可以定义任意个如下格式的特殊函数
这种特殊的函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。
当程序启动的时候,init函数会按照它们声明的顺序自动执行

1
2
3
func init(){
// ...
}

反射

在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的
在Go语言中反射的相关功能由内置的reflect包提供,
任意接口值在反射中都可以理解为由reflect.Typereflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type

TypeOf

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

type myInt int64

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

func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32

type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《跟小王子学Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}

在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 // 底层指针
)

ValueOf

reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息
reflect.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
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(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
}
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)
}

isNil()和isValid()

IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

结构体反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}

func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}

t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}

// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}

并发

Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,
一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度
goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine
合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。

Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。

goroutine

Go语言中使用 goroutine 非常简单,只需要在函数或方法调用前加上go关键字就可以创建一个 goroutine ,从而让该函数或方法在新创建的 goroutine 中执行

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

func hello(i int) {
fmt.Println("hello ", i)
}

func main() {
for i := 1; i <= 100; i++ {
go hello(i)
}
fmt.Println("main")
time.Sleep(1 * time.Second)
}

channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
// 初始化缓冲区为2通道
c := make(chan int, 2)
// 放值
c <- 1
c <- 2
// 取值
x := <-c
fmt.Println(x)
// 关闭通道
close(c)
fmt.Println(c)
// 关闭了 不能放值 但可以取值
x2 := <-c
fmt.Println(x2)
// 没有值取出的 ok 是否 false
x3, ok := <-c
fmt.Println(x3, ok)
}
1
2
3
4
5
6
func f3(ch chan int) {
// for range取值
for v := range ch {
fmt.Println(v)
}
}

单向通道

1
2
<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
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
// Producer2 返回一个接收通道
func Producer2() <-chan int {
ch := make(chan int, 2)
// 创建一个新的goroutine执行发送数据的任务
go func() {
for i := 0; i < 10; i++ {
if i%2 == 1 {
ch <- i
}
}
close(ch) // 任务完成后关闭通道
}()

return ch
}

// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
sum := 0
for v := range ch {
sum += v
}
return sum
}

func main() {
ch2 := Producer2()

res2 := Consumer2(ch2)
fmt.Println(res2) // 25
}

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 <- 10:
//...
default:
//默认操作
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
ch := make(chan int, 1)
for i := 1; i <= 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}

并发安全和锁

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源
Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。

方法名 功能
func (m *Mutex) Lock() 获取互斥锁
func (m *Mutex) Unlock() 释放互斥锁
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
import (
"fmt"
"sync"
)

// sync.Mutex

var (
x int64

wg sync.WaitGroup // 等待组

m sync.Mutex // 互斥锁
)

// add 对全局变量x执行5000次加1操作
func add() {
for i := 0; i < 5000; i++ {
m.Lock() // 修改x前加锁
x = x + 1
m.Unlock() // 改完解锁
}
wg.Done()
}

func main() {
wg.Add(2)

go add()
go add()

wg.Wait()
fmt.Println(x)
}

读写互斥锁

读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine
如果是获取读锁会继续获得锁,如果是获取写锁就会等待
而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待

方法名 功能
func (rw *RWMutex) Lock() 获取写锁
func (rw *RWMutex) Unlock() 释放写锁
func (rw *RWMutex) RLock() 获取读锁
func (rw *RWMutex) RUnlock() 释放读锁
func (rw *RWMutex) RLocker() Locker 返回一个实现Locker接口的读写锁
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
var (
x int64
wg sync.WaitGroup
mutex sync.Mutex
rwMutex sync.RWMutex
)

// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
mutex.Lock() // 加互斥锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
mutex.Unlock() // 解互斥锁
wg.Done()
}

// readWithLock 使用互斥锁的读操作
func readWithLock() {
mutex.Lock() // 加互斥锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
mutex.Unlock() // 释放互斥锁
wg.Done()
}

// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
rwMutex.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwMutex.Unlock() // 释放写锁
wg.Done()
}

// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
rwMutex.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwMutex.RUnlock() // 释放读锁
wg.Done()
}

func do(wf, rf func(), wc, rc int) {
start := time.Now()
// wc个并发写操作
for i := 0; i < wc; i++ {
wg.Add(1)
go wf()
}

// rc个并发读操作
for i := 0; i < rc; i++ {
wg.Add(1)
go rf()
}

wg.Wait()
cost := time.Since(start)
fmt.Printf("x:%v cost:%v\n", x, cost)
}
func main() {
// 使用互斥锁,10并发写,1000并发读
do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s

// 使用读写互斥锁,10并发写,1000并发读
do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms
}

sync

sync.waitGroup

Go语言中可以使用sync.WaitGroup来实现并发任务的同步

方法名 功能
func (wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
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"
"math/rand"
"sync"
"time"
)

func f1(i int, wa *sync.WaitGroup) {
defer wa.Done()
// 随机数 rand.Intn(3)
time.Sleep(time.Second * time.Duration(rand.Intn(3)))
fmt.Println(i)
}

func main() {
// 必须是一个WaitGroup
var wa sync.WaitGroup
for i := 0; i < 10; i++ {
go f1(i, &wa)
wa.Add(1)
}
wa.Wait()
fmt.Println("main")
}

sync.Once

在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Once,sync.Once只有一个Do方法

1
func (o *Once) Do(f func())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package singleton

import (
"sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}

sync.Map

Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map
开箱即用表示其不用像内置的 map 一样使用 make
函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法

方法名 功能
func (m *Map) Store(key, value interface{}) 存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool) 查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) 查询并删除key
func (m *Map) Delete(key interface{}) 删除key
func (m *Map) Range(f func(key, value interface{}) bool) 对map中的每个key-value依次调用f
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"
)

// 并发安全的map
var m = sync.Map{}

func main() {
wg := sync.WaitGroup{}
// 对m执行20个并发的读写操作
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 存储key-value
value, _ := m.Load(key) // 根据key取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

原子操作

针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全
通常直接使用原子操作比使用锁操作效率更高。Go语言中原子操作由内置的标准库sync/atomic提供

方法名 功能
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped 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
package main

import (
"fmt"
"sync"
"sync/atomic"
"time"
)

type Counter interface {
Inc()
Load() int64
}

// 普通版
type CommonCounter struct {
counter int64
}

func (c CommonCounter) Inc() {
c.counter++
}

func (c CommonCounter) Load() int64 {
return c.counter
}

// 互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}

func (m *MutexCounter) Inc() {
m.lock.Lock()
defer m.lock.Unlock()
m.counter++
}

func (m *MutexCounter) Load() int64 {
m.lock.Lock()
defer m.lock.Unlock()
return m.counter
}

// 原子操作版
type AtomicCounter struct {
counter int64
}

func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}

func main() {
c1 := CommonCounter{} // 非并发安全
test(c1)
c2 := MutexCounter{} // 使用互斥锁实现并发安全
test(&c2)
c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
test(&c3)
}

相关文章

GO单元测试

GO常用标准库

GO Model

GO操作Elasticsearch