Go语言

 ·  ☕ 39  · 👀...

变量

 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
package main

// 演示:声明变量

import (
	"fmt"
)

// 声明变量-标准声明
var name string
var age int
var isOk bool

// 批量声明变量
var (
	a string
	b int
	c bool
	d float32
)

func main() {
	// 变量赋值
	name = "王丽兵"
	age = 36
	isOk = true
	fmt.Printf("name:%s", name)

	//注意:局部变量声明,必须使用。

	//类型推导(根据值判断该变量是什么类型)
	var s1 = "wlb"
	fmt.Println(s1)

	// 简短变量声明(只能在函数中使用,推荐使用)
	s2 := "简短变量声明"
	fmt.Println(s2)

	// 匿名变量
	// 在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量,匿名变量用一个下划线表示
	// 匿名变量不占用命名空间,不会分配内存,也不存在重复声明。

	x, _ := foo()
	fmt.Println(x)
}

func foo() (int, string) {
	return 10, "myfood"
}

常量

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

// 声明常量,常量定义以后在程序运行期间不能修改
const pi = 3.1415
const e = 2.7182

// 多常量声明
const (
	statusOk = 200
	notFound = 404
)

// const同时声明多个常量时,如果省略了值则表示和上面一行的值相同,例如:
const (
	n1 = 100
	n2
	n3
)

// iota 是 go 语言的常量计数器,只能在常量的表达式中使用。
// iota 在 const 关键字出现时将被重置为0。
// const 中每新增一行常量声明将使用 iota 计数器一次。
// 使用 iota能简化定义,在定义枚举时很有用。
const (
	c1 = iota //0
	c2        //1
	c3        //2
	c4        //3
)

// 插队
const (
	d1 = iota //0
	d2 = 100  //100
	d3        //100
	d4        //100
)

// 多个常量声明在一行
const (
	e1, e2 = iota + 1, iota + 2 //0+1,0+2
	e3, e4 = iota + 1, iota + 2 //1+1,1+2
)

// 定义数量级
const (
	_  = iota
	KB = 1 << (10 * iota)
	MG = 1 << (10 * iota)
	GB = 1 << (10 * iota)
	TB = 1 << (10 * iota)
	PB = 1 << (10 * iota)
)

func main() {
	fmt.Println("n1:", n1)
	fmt.Println("n2:", n2)
	fmt.Println("n3:", n3)

	fmt.Println("d1:", d1)
	fmt.Println("d2:", d2)
	fmt.Println("d3:", d3)
	fmt.Println("d4:", d4)

	fmt.Println("e1:", e1)
	fmt.Println("e2:", e2)
	fmt.Println("e3:", e3)
	fmt.Println("e4:", e4)
}

基本数据类型

Go语言内置七类基本数据类型

数据类型 说明
布尔类型 bool
整型 byte int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr
浮点型 float32 float64
复数 complex64 complex128
字符 rune
字符串 string
错误类型 error

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
// 演示:整形

package main

import "fmt"

func main() {
	// 十进制
	i1 := 101
	fmt.Printf("%d\n", i1)
	fmt.Printf("%b\n", i1) //把十进制转化成二进制
	fmt.Printf("%o\n", i1) //把十进制转化成八进制
	fmt.Printf("%x\n", i1) //把十进制转化成十六进制

	// 八进制
	i2 := 077
	fmt.Printf("%d\n", i2)

	// 十六进制
	i3 := 0x1234567
	fmt.Printf("%d\n", i3)

	//查看变量类型
	fmt.Printf("%T\n", i3)
}

float

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 演示:浮点数
package main

import (
	"fmt"
)

func main() {
	// go语言中的小数默认是float64类型
	f1 := 1.23456
	fmt.Printf("%T", f1)
}

string

 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
// 演示:字符串

package main

import (
	"fmt"
	"strings"
)

// 字符串用双引号包裹
var s1 = "字符串用双引号包裹"

// 字符用单引号包裹
var c1 = 'h'

func main() {
	// 转义
	path := "d:\\program files\\"
	fmt.Println(path)

	// 使用 `` 原样输出
	s1 := `d:\program files\`
	fmt.Println(s1)

	// 多行字符串
	s2 := `
		窗前明月光
		疑是地上霜
	`
	fmt.Println(s2)

	//字符串长度
	fmt.Println(len(s2))

	// 字符串拼接 + 或fmt.Printf

	// 分割
	ret := strings.Split(path, "")
	fmt.Println(ret)

	// 判断是否包含
	isContains := strings.Contains(s2, "窗")
	fmt.Println(isContains)

	// 前缀/后缀判断
	fmt.Println(strings.HasPrefix(s1, "d:\\"))
	fmt.Println(strings.HasSuffix(s1, "d:\\"))

	// 子串出现的位置
	s4 := "abcdefghijklmnopqrstuvwxyzc"
	fmt.Println(strings.Index(s4, "c"))
	fmt.Println(strings.LastIndex(s4, "c"))
	// join操作
	fmt.Println(strings.Join(ret, "--"))
}

复合数据类型

数组

 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
// 演示:数组
package main

import "fmt"

func main() {
	// 必须制定存放元素的类型和容量(长度)
	var a1 [3]bool
	var a2 [4]bool

	fmt.Printf("a1:%T a2:%T\n", a1, a2)

	// 如果不初始化,默认元素都是零值
	fmt.Println(a1, a2)

	// 初始化方式1
	a1 = [3]bool{true, true, true}
	fmt.Println(a1)

	// 初始化方式2(根据初始值自动推断数组的长度是多少)
	a3 := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8}
	fmt.Println(a3)

	// 初始化方式3(根据索引初始化)
	a4 := [5]int{0: 1, 4: 2}
	fmt.Println(a4)

	var citys = [...]string{"北京", "上海", "深圳"}
	// 数组遍历方法1:for循环 根据索引遍历
	for i := 0; i < len(citys); i++ {
		fmt.Println(citys[i])
	}

	// 数组遍历方法2:for range遍历
	for index, value := range citys {
		fmt.Println(index, value)
	}

	//多维数组
	// [[1 2] [3 4] [5 6]]
	var a5 [3][2]int
	a5 = [3][2]int{
		[2]int{1, 2},
		[2]int{3, 4},
		[2]int{5, 6},
	}
	fmt.Println(a5)

	// 多维数组的遍历
	for _, v1 := range a5 {
		fmt.Println(v1)
		for _, v2 := range v1 {
			fmt.Println(v2)
		}
	}

	// 数组是值类型
	b1 := [3]int{1, 2, 3}
	b2 := b1
	b2[0] = 100
	fmt.Println(b1, b2)

	// 数组求和
	a6 := [...]int{1, 3, 5, 7, 9}
	sum := 0
	for _, v := range a6 {
		sum = sum + v
	}
	fmt.Println(sum)

	// 找出数组中和为指定值的两个元素的下标,比如找出和为8的两个元素的下标
	a7 := [...]int{1, 3, 5, 7, 9}
	// 定义两个for循环,外层的从第一个开始遍历
	// 内层的for循环从外层后面的那个开始找
	for i := 0; i < len(a7); i++ {
		for j := i + 1; j < len(a7); j++ {
			if a7[i]+a7[j] == 8 {
				fmt.Println(i, j)
			}
		}
	}
}

切片(slice)

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址大小容量
切片一般用于快速地操作一块数据集合。
扩容策略如下:

  • 每次只追加一个元素,每一次都是上一次的2倍
  • 追加的超过原来容量的1倍,就等于原来的容量+扩容元素个数最接近的偶数
  • 如果切片的容量大于1024,后续就每次扩容0.25倍

切片的定义语法

1
var name []T

声明切片方式1:基于数组创建切片

1
2
// 声明切片方式1:基于数组创建切片
a := []int{1,2,3}

声明切片方式2:从数组得到切片

1
2
3
4
5
6
// 声明切片方式2:从数组得到切片
var a = [3]int{1,2,3}	//数组
var b []int 			//切片
c = a[0:2]              // []代表从0到3,[:2]代表从0到2  [1:] 代表1到3
fmt.Printlf("c:%T\n",c)
fmt.Println(c)

向切片中添加元素

1
2
a := []string{"北京","上海","深圳","广州"}
a = append(a,"成都")

从切片中删除元素

1
2
3
4
a := []string{"北京","上海","深圳","广州"}
// a[:1] = ["北京","上海"]
// a[2:] = ["深圳","广州"]
a = append(a[:1],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
 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
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
// 演示:Slice
package main

import (
	"fmt"
	"sort"
)

func main() {
	// Slice 是一个拥有相同类型元素的可变长度序列

	// 定义 slice
	var s1 []int
	var s2 []string
	fmt.Println(s1, s2)
	fmt.Println(s1 == nil) //true
	fmt.Println(s2 == nil) //true

	// 初始化 slice
	s1 = []int{1, 2, 3}
	s2 = []string{"北京", "上海", "广州"}
	fmt.Println(s1, s2)
	fmt.Println(s1 == nil) //false
	fmt.Println(s2 == nil) //false

	// 长度和容量
	fmt.Printf("len(s1):%d cap(s1):%d\n", len(s1), cap(s1))
	fmt.Printf("len(s2):%d cap(s2):%d\n", len(s2), cap(s2))

	// 由数组得到切片
	a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	s3 := a1[0:4] //基于一个数组切割,左包含右不包含(左闭右开)
	fmt.Println(s3)
	s4 := a1[1:6]
	fmt.Println(s4)
	s5 := a1[:4] //=>[0:4]
	s6 := a1[3:] //=>[3:len(a1)]
	s7 := a1[:]  //=>[0:len(a1)]
	fmt.Println(s5, s6, s7)

	// 切片的容量从底层数组切片的第一个元素到最后一个元素数量
	fmt.Printf("len(s5):%d cap(s5):%d\n", len(s5), cap(s5)) // 4 9
	fmt.Printf("len(s6):%d cap(s6):%d\n", len(s6), cap(s6)) // 6 6

	// 切片再切片
	s8 := s6[3:]
	fmt.Printf("len(s8):%d cap(s8):%d\n", len(s8), cap(s8)) // 3 3

	// 切片是引用类型,都指向了底层的一个数组
	fmt.Println("s6: ", s6)
	a1[6] = 1300
	fmt.Println("s6: ", s6)
	fmt.Println("s8: ", s8)

	// make()函数创建切片
	s9 := make([]int, 5, 10) //长度是5,容量是10
	// s9 := make([]int, 5)  //长度是5,容量是5
	fmt.Printf("s9=%v len(s1)=%d cap(s9)=%d\n", s9, len(s9), cap(s9))

	s10 := make([]int, 0, 10) //长度是0,容量是10
	fmt.Printf("s10=%v len(s10)=%d cap(s10)=%d\n", s10, len(s10), cap(s10))

	// 注意,切片不能比较,一个nil值的切片没有底层数组
	// 一个nil的切片长度和容量都是0,但是不能说一个长度和容量为0的切片一定是nil。
	// 要判断一个切片是否是空的,要使用 len(s)==0 来判断,不应该使用 s == nil 来判断

	// 切片扩容
	s11 := []string{"北京", "上海", "广州"}
	fmt.Printf("s11=%v len(s11)=%d cap(s11)=%d\n", s11, len(s11), cap(s11))
	// 调用append函数必须用原来的切片变量接收返回值
	s11 = append(s11, "深圳")
	fmt.Println(s11)
	fmt.Printf("s11=%v len(s11)=%d cap(s11)=%d\n", s11, len(s11), cap(s11))
	s11 = append(s11, "天津", "雄安")
	fmt.Printf("s11=%v len(s11)=%d cap(s11)=%d\n", s11, len(s11), cap(s11))

	// copy 复制切片
	s12 := []int{1, 3, 5}
	s13 := s12
	var s14 = make([]int, 3, 3)
	copy(s14, s12) // 使用 copy() 函数将切片 s12 中的元素 复制到 s14
	fmt.Println(s12, s13, s14)
	s12[0] = 100
	fmt.Println(s12, s13, s14)

	// 从切片中删除元素
	/// Go 语言中没有删除切片元素的专用方法
	/// 示例:将s15中索引为1的3 这个元素删除掉
	s15 := []int{1, 3, 5}
	s15 = append(s15[:1], s15[2:]...) // ...表示拆开
	fmt.Println(s15)
	fmt.Printf("s15=%v len(s15)=%d cap(s15)=%d\n", s15, len(s15), cap(s15))

	// 切片练习题
	s16 := make([]int, 5, 10)
	for i := 0; i < 10; i++ {
		s16 = append(s16, i)
	}

	fmt.Println(s16)      // [0 0 0 0 0 1 2 3 4 5 6 7 8 9]
	fmt.Println(cap(s16)) //20

	s17 := [...]int{3, 5, 2, 6, 9}
	sort.Ints(s17[:]) //将数组转化为切片后排序
	fmt.Println(s17)
}

指针

 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"

// 演示:指针
func main() {
	// 1. &:取地址

	n := 18
	p := &n
	fmt.Println(p)
	fmt.Printf("%T\n", p)

	// 2. *:根据地址取值
	m := *p
	fmt.Println(m)
	fmt.Printf("%T\n", m)

	// 3. make 和 new 的区别
	// make 和 new 都是用来申请内存的
	// new 很少用,一般用来给基本数据类型申请内存,string、int 返回的是对应类型的指针(*string、*int)
	// make 是用来给 slice、map、chan 申请内存的,make 函数返回的是对应这三个类型本身
	// make 也是用于分配内存的,区别于 new,它只用于 slice、map、chan 的内存创建,
	// 而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
}

map

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

// 演示:map
func main() {
	// map 是一种无序的基于 key-value 的数据结构,Go 语言中的 map 是引用类型,必须初始化才能使用
	var m1 map[string]int
	m1 = make(map[string]int, 10) // 要估算好该map容量,避免在程序运行期间再次扩容
	m1["张三"] = 18
	m1["李四"] = 19
	fmt.Println(m1)
	fmt.Println(m1["张三"])

	value, ok := m1["王五"]

	if !ok {
		fmt.Println("查无此key")
	} else {
		fmt.Println(value)
	}

	// 只遍历key
	for k := range m1 {
		fmt.Println(k)
	}

	// 只遍历value
	for _, v := range m1 {
		fmt.Println(v)
	}

	// 删除
	delete(m1, "李四")
	fmt.Println(m1)
}

struct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 演示:结构体
// 结构体是值类型

package main

import "fmt"

type person struct {
	name   string
	age    int
	hobby  []string
	gender string
}

// go语言中函数传参数永远是传的拷贝
func f(x person) {
	x.gender = "女" //修改的是副本的gender
}

func f2(x *person) {
	(*x).gender = "女" // 根据内存地址找到那个原变量,修改的就是原来的变量
	// 下面写的也是可以的,如果传的是指针,go语言会直接对指针对应的变量进行操作(语法糖)
	// x.gender = "女" // 根据内存地址找到那个原变量,修改的就是原来的变量
}

func main() {
	var p1 person
	p1.name = "张三"
	p1.age = 18
	p1.gender = "男"
	p1.hobby = []string{"篮球", "足球", "双色球"}
	fmt.Printf("type:%T value:%v\n", p1, p1)

	// 匿名结构体,多用于临时场景
	var s struct {
		name string
		age  int
	}
	s.name = "lili"
	s.age = 20
	fmt.Printf("type:%T value:%v\n", s, s)

	f(p1)
	fmt.Println(p1.gender)

	f2(&p1)
	fmt.Println(p1.gender)

	// 创建指针类型的结构体
	// 使用new关键字对结构体进行实例化,得到的是结构体的地址
	var p2 = new(person)
	fmt.Printf("%T\n", p2) //*main.person
	fmt.Printf("p2=%#v\n", p2)

	// 结构体指针2,key-value初始化
	// 使用值列表的形式初始化,值的顺序要和结构体定义字段的顺序一致
	var p3 = &person{
		name: "小王子",
		age:  18,
	}

	fmt.Printf("%p\n", p3)
}

控制语句

if语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 演示: if 流程控制
package main

import "fmt"

func main() {

	age := 19
	if age > 18 {
		fmt.Println("成年了")
	} else {
		fmt.Println("未成年")
	}

	//多条件判断
	if age > 35 {
		fmt.Println("人到中年")
	} else if age > 18 && age < 35 {
		fmt.Println("青年")
	}
}

switch语句

 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
// 演示:switch

package main

import (
	"fmt"
)

func main() {
	var n= 5
	switch n {
	case 1:
		fmt.Println("大拇指")
	case 2:
		fmt.Println("食指")
	case 3:
		fmt.Println("中指")
	case 4:
		fmt.Println("无名指")
	case 5: 
		fmt.Println("小指")
	default:
		fmt.Println("六指")
	}

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

	// 分支使用表达式
	age := 30
	switch{
	case age<25:
		fmt.Println("好好学习")
	case age>25 && age<35:
		fmt.Println("好好工作")
	case age>60:
		fmt.Println("好好享受")
	default:
		fmt.Println("活着真好")
	}

	// fallthrough 语法可以执行满足条件的下一个case,是为了兼容C语言中的case设计的
	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("...")
	}
}

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
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
// 演示:for循环
package main

import "fmt"

func main() {
	//基本格式
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}

	//变种1
	// var i = 5
	// for ; i < 10; i++ {
	// 	fmt.Println(i)
	// }

	//变种2
	// var i = 5
	// for i < 10 {
	// 	fmt.Println(i)
	// 	i++
	// }

	// 无限循环
	// for {
	// fmt.Println("无限循环循环体")
	// }

	// for range循环
	s := "hello,王丽兵"
	for i, v := range s {
		fmt.Printf("%d %c\n", i, v)
	}

	// 跳出循环(当i=5时)
	for i := 0; i < 10; i++ {
		if i == 5 {
			break
		}
		fmt.Println(i)
	}
	fmt.Println("over")

	// 跳过此次循环,继续执行下一次循环(当i=5时)
	for i := 0; i < 10; i++ {
		if i == 5 {
			continue //继续执行下一次循环
		}
		fmt.Println(i)
	}
	fmt.Println("over")
}

goto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 演示:goto 但不推荐使用goto
package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				goto breakTag
			}
			fmt.Printf("%v-%v\n", i, j)
		}
	}
	return

	//标签
breakTag:
	fmt.Println("结束")
}

函数

 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

// 函数定义,返回值命名为ret
func f1(x, int, y int) (ret int) {
	return x + y
}

// 没有返回值的函数
func f2(x int, y int) {

}

// 没有参数、没有返回值的函数
func f3() {

}

// 返回值命名为ret不用写return。返回值可以命名,也可以不命名
// func f4(x int, y int) (ret int) {
// 	ret = x + y
// }

// 多个返回值
func f5() (int, int) {
	return 1, 2
}

// 参数的类型简写
/// 当参数中连续两个或多个参数的类型一致时,可以将非最后一个的参数类型省略
// func f6(x, y int, m, n string, i, j bool) int {

// }

// 可变长参数,y的类型是切片
// 注意:可变长参数必须放在函数参数的最后
func f7(x int, y ...int) {

}

// Go语言中函数没有默认参数这个概念

// 演示:函数
func main() {

}

defer

 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"

// 演示:defer
// 一个函数中可以有多个defer语句,多个defer按照先进后出,后进先出的顺序延迟执行
// defer 多用于函数结束之前释放资源(文件句柄、数据库连接、socket)

// Go语言中函数的return不是原子操作,在底层是分为两步来执行
// 第一步:返回值赋值
// 第二步:真正的RET返回
// 函数中如果存在defer,那么defer执行的时机是在第一步和第二步之间

func deferDemo() {
	fmt.Println("start")
	defer fmt.Println("111") // 把它后面的语句延迟到函数即将返回的时候再执行
	defer fmt.Println("222") //多个defer按照先进后出,后进先出的顺序延迟执行
	defer fmt.Println("333")
	fmt.Println("end")
}

func main() {
	deferDemo()
}

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
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

// 演示:panic/recover
// Go语言目前没有异常机制,但是使用panic/recover 模式来处理错误。
// panic 可以在任何地方引发,但 recover只有在defer调用的函数中有效。
// recover() 必须搭配defer使用
// defer一定要在可能引发panic的语句之前定义

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	// 刚刚打开数据库连接
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
		fmt.Println("释放数据库连接")
	}()
	panic("出现了严重的错误!!!")
	fmt.Println("func B")
}

func funcC() {
	fmt.Println("func C")
}

func main() {
	funcA()
	funcB()
	funcC()
}

接口

基本用法

 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
// 演示:接口
// 使用值接收者实现接口与使用指针接收者实现接口的区别?
// 使用值接收者实现接口,结构体类型和结构体指针类型的变量都能存
// 指针接收者实现接口只能存结构体指针类型的变量

package main

import "fmt"

type cat struct {
}

type dog struct {
}

type person struct {
}

//定义一个能叫的类型
type speaker interface {
	speak() // 方法签名,可以有一个,也可以有多个
}

// cat实现了speaker接口
func (c cat) speak() {
	fmt.Println("喵喵喵")
}

// dog实现了speaker接口
func (d dog) speak() {
	fmt.Println("汪汪汪")
}

//person实现了speaker接口
func (p person) speak() {
	fmt.Println("啊啊啊")
}

// 接收一个参数,传进来什么,就打什么,被打的开始speak
func da(x speaker) {
	x.speak()
}

func main() {
	var c1 cat
	var d1 dog
	var p1 person

	// da(c1)
	// da(d1)
	// da(p1)

	var ss speaker
	ss = c1
	da(ss)
	ss = d1
	da(ss)
	ss = p1
	da(ss)

	fmt.Println(ss)
}

只要实现了接口中的所有方法,就实现了这个接口。

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 演示:空接口

package main

func main() {
	var m1 map[string]interface{}
	m1 = make(map[string]interface{}, 16)
	m1["name"] = "zhangsan"
	m1["age"] = 22
	m1["merried"] = true
	m1["hobby"] = [...]string{"唱", "跳", "rap"}
}

空接口作为函数的参数

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

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

// type 后面跟的是类型
type myInt int     // 自定义类型
type yourInt = int // 类型别名

func main() {
	var n myInt
	n = 100
	fmt.Printf("%T \n", n)

	var m yourInt
	m = 100
	fmt.Printf("%T \n", m)

	var c rune
	c = '中'
	fmt.Println(c)
	fmt.Printf("%T \n", c)
}
  • 自定义类型是一种新的类型,而类型别名是指向该类型的指针。
  • 类型别名仅仅是为了提高程序的可读性。

类型断言

Demo1

 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
// 演示:类型断言

package main

import "fmt"

// 类型断言1
func assign1(i interface{}) {
	fmt.Printf("%T\n", i)
	str, ok := i.(string)
	if !ok {
		fmt.Println("猜错了")
	} else {
		fmt.Println("传进来的是一个字符串:", str)
	}
}

// 类型断言2
func assign2(i interface{}) {
	fmt.Printf("%T\n", i)
	switch t := i.(type) {
	case string:
		fmt.Println("是一个字符串", t)
	case int:
		fmt.Println("是一个int:", t)
	case int64:
		fmt.Println("是一个int64:", t)
	case bool:
		fmt.Println("是一个bool:", t)
	}
}

func main() {
	assign2(true)
}

Demo2

 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!")
	}
}

显示类型转换

1
2
3
4
5
6
7
8
// string到int
int,err:=strconv.Atoi(string)
// string到int64
int64, err := strconv.ParseInt(string, 10, 64)
// int到string
string:=strconv.Itoa(int)
// int64到string
string:=strconv.FormatInt(int64,10)

结构体

定义结构体语法

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

实例化结构体

基本的实例化形式

1
var ins T

示例:

1
2
3
4
5
6
7
8
type Point struct {
    X int
    Y int
}

var p Point
p.X = 10
p.Y = 20

创建指针类型的结构体

Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

1
ins := new(T)

其中:

  • T 为类型,可以是结构体、整型、字符串等。
  • ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。

示例:

1
2
3
4
5
6
7
8
9
type Player struct{
    Name string
    HealthPoint int
    MagicPoint int
}

tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300

取结构体的地址实例化

在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:

1
ins := &T{}

其中:

  • T 表示结构体类型。
  • ins 为结构体的实例,类型为 *T,是指针类型。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Command struct {
    Name    string    // 指令名称
    Var     *int      // 指令绑定的变量
    Comment string    // 指令的注释
}

var version int = 1

cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"

取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func newCommand(name string, varref *int, comment string) *Command {
    return &Command{
        Name:    name,
        Var:     varref,
        Comment: comment,
    }
}

cmd = newCommand(
    "version",
    &version,
    "show version",
)

指针

  • &:表示取地址
  • *:根据地址取值

make 和 new

1
2
3
4
// 错误写法
var a *int 	
//正确写法
var a = new(int) 
  • new 用于初始化值类型指针的。
  • make 用于引用类型(slice、map、channel)的初始化(申请内存空间)。

初始化结构体

使用“键值对”初始化结构体

1
2
3
4
5
ins := 结构体类型名{
    字段1: 字段1的值,
    字段2: 字段2的值,
    
}

使用多个值的列表初始化结构体

Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。

1
2
3
4
5
ins := 结构体类型名{
    字段1的值,
    字段2的值,
    
}

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

  • 必须初始化结构体的所有字段。
  • 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 键值对与值列表的初始化形式不能混用。

初始化匿名结构体

匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ins := struct {
    // 匿名结构体字段定义
    字段1 字段类型1
    字段2 字段类型2
    
}{
    // 字段值初始化
    初始化字段1: 字段1的值,
    初始化字段2: 字段2的值,
    
}

模拟构造函数

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

简单模拟构造函数示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Cat struct {
    Color string
    Name  string
}

func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}

func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

带有父子关系的结构体的构造和初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Cat struct {
    Color string
    Name  string
}

type BlackCat struct {
    Cat  // 嵌入Cat, 类似于派生
}

// “构造基类”
func NewCat(name string) *Cat {
    return &Cat{
        Name: name,
    }
}

// “构造子类”
func NewBlackCat(color string) *BlackCat {
    cat := &BlackCat{}
    cat.Color = color
    return cat
}

递归

 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
// 演示:递归
// 递归一定要有一个明确的退出条件
// 递归适合处理那种问题相同、规模越来越小的场景

package main

import "fmt"

// 计算 n 的阶乘
func f(n uint64) uint64 {
	if n <= 1 {
		return 1
	}
	return n * f(n-1)
}

// n 个台阶,一次可以走1步,也可以走2步,有多少种走法
func taijie(n uint64) uint64 {
	if n == 1 {
		// 如果只有一个台阶,就一种走法
		return 1
	}
	if n == 2 {
		return 2
	}
	return taijie(n-1) + taijie(n-2)
}

func main() {
	ret := f(5)
	fmt.Println(ret)

	ret2 := taijie(4)
	fmt.Println(ret2)
}

并发

并发模型

CSP简介

《Communicating Sequential Process》(CSP)是计算机科学领域的大牛“托尼 霍尔”(C.A.R.Hoare)于1978年发表的一篇论文,后期不断优化最终发展为一个代数理论,用来描述并发系统消息通信模型并验证其正确性。
其最本质的思想是:将并发系统抽象为Channel和Process两部分,Channel用来传递消息,Process用于执行,Channel和Process之间相互独立,没有从属关系,消息的发送和接收有严格的时序限制。
Go语言主要借鉴了Channel和Process的概念,在Go中Channel就是通道,Process就是goroutine。

调度模型

应用程序的调度模型有以下三种:

  • 多进程模型
    进程都能够被多核CPU并发调度,优点是每个进程都有自己独立的内存空间,隔离性好、健壮性高;缺点是进程比较重,进程的切换消耗较大,进程间的通信需要多次在内核区和用户区之间复制数据。
  • 多线程模型
    这里的多线程是指启动多个线程进行处理,线程的优点是通过共享内存进行通信更快捷,切换代价小;缺点是多个线程共享内存空间,极易导致数据访问混乱,某个线程误操作内存挂掉可能危及整个线程组,健壮性不高。
  • 用户级多线程模型
    用户级线多线程又分为两种情况,一种是M:1的方式,M个用户线程对应一个内核进程,这种情况很容易因为一个系统阻塞,其它用户线程都会被阻塞,不能利用机器多核的优势。还有一种模式就是M:N的方式,M个用户线程对应N个内核线程,这种模式一般需要语言运行时或库的支持,效率最高。
    程序并发处理的要求越来越高,但是不能无限制地增加系统线程数,线程数过多会导致操作系统的调度开销变大,单个线程的单位时间内被分配的运行时间片减少,单个线程的运行速度降低,单靠增加系统线程数不能满足要求。为了不让系统线程无限膨胀,于是就又了协程的概念。协程是一种用户态的轻量级线程,协程的调度完全由用户态程序控制,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其它地方,在切换回来的时候,恢复先前保存的寄存器和上下文栈,每个内核线程可以对应多个用户协程,当一个协程执行体阻塞了,调度器会调度另一个协程执行,最大效率地利用操作系统分给系统线程的时间片。前面提到的用户级多线程模型就是一种协程模型,尤其以M:N模型最为高效。
    这样的好处显而易见:
    (1)控制了系统线程数,保证每个线程的运行时间片充足。
    (2)调度层能进行用户态的切换,不会导致单个协程阻塞整个程序的情况,尽量减少上下文切换,提升运行效率。
    由此可见,协程是一种非常高效、理想的执行模型。Go的并发执行模型就是一种变种的协程模型。

启动单个goroutine

1
2
3
4
5
6
7
func hello() {
	fmt.Println("Hello Goroutine!")
}
func main() {
	hello()
	fmt.Println("main goroutine done!")
}

启动多个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
package main

import (
	"fmt"
	"math/rand" //注意这里,是math包
	"sync"
	"time"
)

func f() {
	// 随机数种子
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < 5; i++ {
		r1 := rand.Int()
		r2 := rand.Intn(10)
		fmt.Println(r1, r2)
		fmt.Println(0-r1, 0-r2) //随机负数
	}
}

func f2(i int) {
	defer wg.Done()
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(300)))
	fmt.Println(i)
}

func hello() {
	fmt.Println("helo")
}

var wg sync.WaitGroup

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go f2(i)
	}
	wg.Wait() // 等待wg的计数器减为0
}

channel

channel类型

1
2
3
4
5
6
7
// 语法
var 变量 chan 元素类型

//示例
var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

channel操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
//示例
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

//创建channel
var ch chan int
ch := make(chan int)

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

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

//关闭
close(ch)

无缓冲channel

无缓冲的通道又称为阻塞的通道,无缓冲的通道只有在有人接收值的时候才能发送值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

有缓冲channel

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

从channel循环取值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	// 开启goroutine将0~100的数发送到ch1中
	go func() {
		for i := 0; i < 100; i++ {
			ch1 <- i
		}
		close(ch1)
	}()
	// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
	go func() {
		for {
			i, ok := <-ch1 // 通道关闭后再取值ok=false
			if !ok {
				break
			}
			ch2 <- i * i
		}
		close(ch2)
	}()
	// 在主goroutine中从ch2中接收值打印
	for i := range ch2 { // 通道关闭后会退出for range循环
		fmt.Println(i)
	}
}

单向channel

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作
func counter(out chan<- int) {     
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}

// <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作
func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {
	for i := range in {
		fmt.Println(i)
	}
}

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go counter(ch1)
	go squarer(ch2, ch1)
	printer(ch2)
}

worker pool(goroutine池)

在工作中我们通常会使用可以指定启动的goroutine数量–worker pool模式,控制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
func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Printf("worker:%d start job:%d\n", id, j)
		time.Sleep(time.Second)
		fmt.Printf("worker:%d end job:%d\n", id, j)
		results <- j * 2
	}
}


func main() {
	jobs := make(chan int, 100)
	results := make(chan int, 100)
	// 开启3个goroutine
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}
	// 5个任务
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)
	// 输出结果
	for a := 1; a <= 5; a++ {
		<-results
	}
}

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

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
func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

并发安全和锁

互斥锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加锁
		x = x + 1
		lock.Unlock() // 解锁
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

读写互斥锁

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

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

sync.WaitGroup

Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法 说明
(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
var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案sync.Once

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
	icons = map[string]image.Image{
		"left":  loadIcon("left.png"),
		"up":    loadIcon("up.png"),
		"right": loadIcon("right.png"),
		"down":  loadIcon("down.png"),
	}
}

// Icon 是并发安全的
func Icon(name string) image.Image {
	loadIconsOnce.Do(loadIcons)
	return icons[name]
}

下面是借助sync.Once实现的并发安全的单例模式:

 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语言中内置的map不是并发安全的。请看下面的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			set(key, n)
			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var m = sync.Map{}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)
			value, _ := m.Load(key)
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

原子操作

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

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用channel或者sync包的函数/类型实现同步更好。

反射

反射介绍

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

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

Go程序在运行期使用reflect包访问程序的反射信息。

reflect包

在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

TypeOf

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

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

import (
	"fmt"
	"reflect"
)

func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(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
package main

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
}

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

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与原始值之间可以互相转换。

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
 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
}
通过反射设置变量的值

想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的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)
}
isNil()和isValid()
1
func (v Value) IsNil() bool

IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。

1
func (v Value) IsValid() bool

IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。

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

结构体反射

与结构体相关的方法

任意值通过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类型

结构体反射示例

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

 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:  "Kevin",
		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"))
	}
}

函数printMethod(s interface{})来遍历打印s包含的方法:

 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
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
	msg := "好好学习,天天向上。"
	fmt.Println(msg)
	return msg
}

func (s student) Sleep() string {
	msg := "好好睡觉,快快长大。"
	fmt.Println(msg)
	return msg
}

func printMethod(x interface{}) {
	t := reflect.TypeOf(x)
	v := reflect.ValueOf(x)

	fmt.Println(t.NumMethod())
	for i := 0; i < v.NumMethod(); i++ {
		methodType := v.Method(i).Type()
		fmt.Printf("method name:%s\n", t.Method(i).Name)
		fmt.Printf("method:%s\n", methodType)
		// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
		var args = []reflect.Value{}
		v.Method(i).Call(args)
	}
}

命令行参数解析

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

func main()  {
    // 定义几个变量,用于接收命令行的参数值
    var user string
    var pwd string
    var host string
    var port int

    // &user 就是接收用户命令行中输入的 -u 后面的参数值
    // "u" 就是 -u 指定的参数
    // "" 默认值
    // "用户名,默认为空" 说明
    flag.StringVar(&user, "u", "", "用户名,默认为空")
    flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
    flag.StringVar(&host, "h", "localhost", "主机名,默认为 localhost")
    flag.IntVar(&port, "port", 3306, "duan端口号,默认3306")

    // 【必须调用】从 arguments 中解析注册的 flag
    flag.Parse()

    // 输出结果
    fmt.Printf("\n user=%v \n pwd=%v \n host=%v \n port=%v \n", user, pwd, host, port)
}
1
> go run main.go -u=root -pwd=123456 -h=127.0.0.1 -port=3306

fmt.Printf

占位符

*printf 系列函数都支持format格式化参数。

通用占位符

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

示例代码:

1
2
3
4
5
6
7
fmt.Printf("%v\n", 100)
fmt.Printf("%v\n", false)
o := struct{ name string }{"kevin"}
fmt.Printf("%v\n", o)
fmt.Printf("%#v\n", o)
fmt.Printf("%T\n", o)
fmt.Printf("100%%\n")

输出结果

100
false
{Kevin}
struct { name string }{name:"kevin"}
struct { name string }
100%

布尔型

占位符 说明
%t true或false

整形

占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

示例代码:

1
2
3
4
5
6
7
n := 65
fmt.Printf("%b\n", n)
fmt.Printf("%c\n", n)
fmt.Printf("%d\n", n)
fmt.Printf("%o\n", n)
fmt.Printf("%x\n", n)
fmt.Printf("%X\n", n)

输出结果

1000001
A
65
101
41
41

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

示例代码:

1
2
3
4
5
6
7
f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)

输出结果:

6946802425218990p-49
1.234000e+01
1.234000E+01
12.340000
12.34
12.34

字符串和[]byte

占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f)
%X 每个字节用两字符十六进制数表示(使用A-F)

代码示例

1
2
3
4
5
s := "Kevin"
fmt.Printf("%s\n", s)
fmt.Printf("%q\n", s)
fmt.Printf("%x\n", s)
fmt.Printf("%X\n", s)

输出结果:

Kevin
"Kevin"
e5b08fe78e8be5ad90
E5B08FE78E8BE5AD90

指针

占位符 说明
%p 表示为十六进制,并加上前导的0x

代码示例

1
2
3
a := 10
fmt.Printf("%p\n", &a)
fmt.Printf("%#p\n", &a)

输出结果:

0xc000094000
c000094000

宽度标识符

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

代码示例

1
2
3
4
5
6
n := 12.34
fmt.Printf("%f\n", n)
fmt.Printf("%9f\n", n)
fmt.Printf("%.2f\n", n)
fmt.Printf("%9.2f\n", n)
fmt.Printf("%9.f\n", n)

输出结果:

12.340000
12.340000
12.34
    12.34
       12

其它

占位符 说明
‘+’ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
'’ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
‘-’ 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
‘#’ 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值;
‘0’ 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;

代码示例

1
2
3
4
5
6
7
8
s := "Kevin"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%-5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)

获取输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
1
2
3
4
5
6
7
8
9
func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scan(&name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
1
2
3
> ./scan_demo 
Kevin 28 false
扫描结果 name:Kevin age:28 married:false

fmt.Scanf

  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。
1
2
3
4
5
6
7
8
9
func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
1
2
3
> ./scan_demo 
1:Kevin 2:28 3:false
扫描结果 name:Kevin age:28 married:false 

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。

例如,我们还是按照上个示例中以空格分隔的方式输入,fmt.Scanf就不能正确扫描到输入的数据。

1
2
3
> ./scan_demo 
Kevin 28 false
扫描结果 name: age:0 married:false

fmt.Scanln

  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。
1
2
3
4
5
6
7
8
9
func main() {
	var (
		name    string
		age     int
		married bool
	)
	fmt.Scanln(&name, &age, &married)
	fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
1
2
3
> ./scan_demo 
Kevin 28 false
扫描结果 name:Kevin age:28 married:false

fmt.Scanln 遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。示例代码如下:

1
2
3
4
5
6
7
func bufioDemo() {
	reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
	fmt.Print("请输入内容:")
	text, _ := reader.ReadString('\n') // 读到换行
	text = strings.TrimSpace(text)
	fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

1
2
3
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

Sscan系列

这几个函数功能分别类似于fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

1
2
3
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

使用goproxy

1
2
3
4
##### 1. 开启go module支持
> export GO111MODULE=on
##### 2. 设置代理
> export GOPROXY=https://goproxy.cn
1
2
3
4
##### 1. 开启go module支持
> SET GO111MODULE=on
##### 2. 设置代理
> SET GOPROXY="https://goproxy.cn"

参考


Wanglibing
Wanglibing
Engineer,Lifelong learner