Go黑马程序员

Olivia的小跟班 Lv3

1.初识go语言

第一个Go程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.狗语言是以包作为管理单位
// 2.每个文件都必须先声明包
// 3,程序必须有一个main包(重要)
package main

import "fmt" //导入包

//入口函数
func main() { //左括号必须和函数名同行,main函数不带参数
//打印
//hello world打印到屏幕,Println()函数会自动换行
//调用函数,大部分都需要导入包
/* 这也是注释的一种方式 */
fmt.Println("hello world!") //语句结尾不需要加分号

}

命令行运行程序

image-20221211160453985

2.数据类型

命名

计算机用来计算,计算前需要存数,如何存数?

数据类型作用:告诉编译器这个数(变量)应该以多大的内存存储

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

个简单的命名规则:一个名字必须以一个字母(Unicode 字母)或下划线开头,后面可以跟

任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort 和 Heapsort 是两

个不同的名字。

Go 语言中类似 if 和 switch 的关键字有 25 个(均为小写)。关键字不能用于自定义名字,只能

在特定语法结构中使用。

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

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

函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
内建常量:
true false iota nil

内建类型:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error

内建函数:
make len cap new append copy close delete
complex real imag
panic recover

变量的声明

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

import "fmt" //导入包后,必须要使用

func main() {
//变量,程序运行期间,可以改变的量
//1.声明格式: var 变量名 变量类型(变量声明之后,必须要使用)
//2.只是声明没有初始化的变量,默认值为0
//3.同一个{}里,声明的变量名是唯一的
//4.可以同时声明多个变量

var a int
a = 10 //变量的赋值
fmt.Println("a=", a)
}

变量初始化和自动推导类型

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.声明格式: var 变量名 变量类型(变量声明之后,必须要使用)
//2.只是声明没有初始化的变量,默认值为0
//3.同一个{}里,声明的变量名是唯一的
//4.可以同时声明多个变量

var a int
a = 10 //变量的赋值
fmt.Println("a=", a)

//2.变量的初识化,声明变量时,同时赋值
var b int = 10 //初始化,声明变量时,同时赋值(一步到位)
b = 35 //赋值,先声明,后赋值
fmt.Println("b=", b)

//3.自动推导类型,必须初始化,通过初始化的值确定类型(常用)
c := 30
//%T打印变量所属的类型
fmt.Printf("c is type of %T", c)
}

自动推导类型和赋值的区别

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

import "fmt"

func main() {
var a int
//赋值:赋值之前先声明变量
//赋值,可以赋值n次
a = 10
a = 12
a = 13
fmt.Println("a=", a)

//:=,自动推导类型,先声明变量b,再给b赋值为20
//自动推导,同一个变量名只能使用一次,用于初始化那次
b := 20
fmt.Println("b=", b)

//b:=30 会报错,因为已经有一个变量b存在了

b = 30 //只是赋值,可以使用
fmt.Println("b=", b)
}

Println和Printf的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main //  必须有一个main包

import "fmt"

func main() {
a := 10
//一段一段处理,自动加换行
fmt.Println("a=", a)

//格式化输出,把a的内容放在%d的位置
//“a=10\n" 这个字符串输出到屏幕,”\n“表示换行符
fmt.Printf("a=%d\n", a)

b := 20
c := 30
fmt.Println("a=", a, ",b=", b, ",c=", c)
fmt.Printf("a=%d,b=%d,c=%d", a, b, c)
}

image-20221211195102865

多重赋值和匿名变量

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"

func test() (a, b, c int) {
return 1, 2, 3
}

func main() {
//a:=10
//b:=20
//c:=30

a, b := 10, 20
var temp int
temp = a
a = b
b = temp
fmt.Println("a=", a, "b=", b)

//i:=10
//j:=20
i, j := 10, 10
i, j = j, i
fmt.Printf("a=%d,b=%d\n", i, j)

i = 30
j = 20
temp, _ = i, j
fmt.Println("temp=", temp)

var c, d, e int
c, d, e = test()
fmt.Printf("c=%d,d=%d,e=%d\n", c, d, e)

d = 5
_, d, _ = test()
fmt.Printf("d=%d", d)
}

常量的使用

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

import "fmt"

func main() {
//变量: 程序运行期间,可以改变的量,变量声明需要var
//常量: 程序运行期间,不可以改变的量,常量声明需要const
const a int = 10
//a=12 //不能修改常量
fmt.Println("a=", a)

const b = 20 //常量的自动推导类型不需要:=
fmt.Printf("b type is %T\n", b)
fmt.Println("b=", b)
}

多个变量或常量的定义

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

import "fmt"

func main() {
// var a int = 10
// var b float64 = 3.14

var (
a int = 10
b float64 = 3.14
)

var (
a = 10
b = 3.14
)//这个不用加:=就可以自动推导类型

fmt.Println("a=", a)
fmt.Println("b=", b)

// const a int = 10
// const b float64 = 3.14
const (
i int = 10
j float64 = 3.14
)
fmt.Println("i=", i)
fmt.Println("j=", j)

}

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

import "fmt"

func main() {
//1.iota常量自动生成器,每行一个,自动累加1
//2.iota给常量赋值使用
const (
a = iota
b = iota
c = iota
)
fmt.Printf("a=%d,b=%d,c=%d\n", a, b, c)

//3.iota遇到const,重置为0
const d = iota
fmt.Printf("d=%d\n", d)

//4.可以只写一个iota
const (
a1 = iota
b2
c3
)
fmt.Printf("a1=%d,b2=%d,c3=%d\n", a1, b2, c3)

//5.如果是同一行,值都一样
const (
i = iota
j1, j2, j3 = iota, iota, iota
k = iota
)
fmt.Printf("i=%d,j1=%d,j2=%d,j3=%d,k=%d\n", i, j1, j2, j3, k)
}

bool类型

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

import "fmt"

func main() {
//1.声明变量,没有初识化,布尔值默认为false(零值作为初识值)
var b bool
fmt.Println("b=", b)

b = true
fmt.Println("b=", b)

//2.自动推导类型
var c = true
fmt.Println("c=", c)

d := false
fmt.Println("d=", d)
}

浮点数

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

import "fmt"

func main() {
//声明变量
var f1 float64
f1 = 3.14
fmt.Println("f1=", f1)

//自动推导类型
f2 := 31.4
fmt.Printf("f2 type is %T\n", f2)

//float64存储小数比float32更准确
}

字符类型

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

import "fmt"

func main() {
var ch byte //声明字符类型
ch = 97
//fmt.Println("ch=", ch)
//格式化输出,%c以字符方式打印,%d以整型方式打印
fmt.Printf("ch=%c,%d\n", ch, ch)

ch = 'a' //字符是单引号
fmt.Printf("ch=%c,%d\n", ch, ch)

//大写转小写,小写转大写,大小写相差32,小写大
//转义字符以'\'开头,'\n'换行符
fmt.Printf("大写:%d,小写:%d\n", 'A', 'a')
fmt.Printf("大写转小写:%c\n", 'A'+32)
fmt.Printf("小写转大写:%c\n", 'a'-32)
}

字符串类型

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

import "fmt"

func main() {
var str1 string //声明变量
str1 = "abc"
fmt.Println("str1=", str1)

//自动推导类型
str2 := "mike"
fmt.Println("str2=", str2)

//内建函数,len()可以测字符串的长度,有多少个字符
fmt.Println("len(str2)=", len(str2))
}

字符和字符串的区别

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

import (
"fmt"
)

func main() {
var ch byte
var str string

/*字符
1.单引号
2.字符,往往都只有一个字符,转义字符除外(如'\n')
*/
ch = 'a'
fmt.Println("ch=", ch)

/*
字符串
1.双引号
2.字符串中有一个或多个字符组成
3.每个字符串都是隐藏了一个结束符,'\0'
*/
str = "a"
fmt.Println("str=", str)

str = "hell go"
//取下标操作
fmt.Printf("str[0]=%c", str[0])
}

复数类型

image-20221212152103428

格式化输出

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

import "fmt"

func main() {
a := 10
b := 'A'
c := "hello"
d := 3.14
//%T 操作变量所属类型, %d整型格式, %c字符个数 ,%s字符串格式, %f浮点型个数
//%v 自动匹配格式输出
fmt.Printf("%T,%T,%T,%T\n", a, b, c, d)
fmt.Printf("%d,%c,%s,%f\n", a, b, c, d)
fmt.Printf("%v,%v,%v,%v\n", a, b, c, d)
}

变量的输入

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

import "fmt"

func main() {
var a int
var b int
fmt.Printf("请输入变量a:")
fmt.Scanf("%d", &a)
fmt.Printf("请输入变量b:")
fmt.Scan(&b)
fmt.Println("a=", a)
fmt.Println("b=", b)
}

类型转化

image-20221212154458277

类型别名

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

import "fmt"

func main() {
//给int64起一个别名叫bigint
type bigint int64
var a bigint //等价于var a int64
fmt.Printf("a type is %T\n", a)

type (
long int64
char byte
)

var b long = 11
var ch char = 'a'
fmt.Printf("b=%d,ch=%c\n", b, ch)
}

3运算符

image-20221212161257949

image-20221212161302744

image-20221212161307687

image-20221212162315689

只有整除哦

4.流程控制

if的使用,if支持初始化语句

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

import "fmt"

func main() {
s := "王思聪"

//if和{就是条件,条件通常都是关系运算符
if s == "王思聪" { //左括号和if在同一行
fmt.Println("王思聪")
}

//if支持1个初始化语句,初始化语句和判断条件以分号分隔
if a := 10; a == 10 { //条件为真,指向{}里面
fmt.Println("a==10")
}
}

if else_if else的用法

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

import "fmt"

func main() {
//1
if a := 10; a == 10 {
fmt.Println("a==10")
} else if a > 10 {
fmt.Println("a>10")
} else if a < 10 {
fmt.Println("a<10")
}

//2
b := 15
if b == 10 {
fmt.Println("b==10")
} else if b > 10 {
fmt.Println("b>10")
} else if b < 10 {
fmt.Println("b<10")
}

//3
c := 5
if c == 10 {
fmt.Println("c==10")
} else if c > 10 {
fmt.Println("c>10")
} else {
fmt.Println("c<10")
}

//4
d := 12
if d == 12 {
fmt.Println("d==12")
}
if d > 12 {
fmt.Println("d>12")
}
if d < 12 {
fmt.Println("d<12")
}
}

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

import "fmt"

func main() {
num := 12
switch num { //switch后面写的是变量本身
case 1:
fmt.Println("按下的是1楼")
//break——>go语言保留了break关键字,用于跳出switch语句,可以不用写break,默认就包含
//fallthrough 不会跳出switch语句,后面case下的语句无条件执行
case 2:
fmt.Println("按下的是2楼")
case 3:
fmt.Println("按下的是3楼")
case 4:
fmt.Println("按下的是4楼")
default:
fmt.Println("按下的是xxx楼")
}

//补充switch后面可以没有变量(即是没有条件)
//case后面可以是条件,也可以是多个值一起(以,分隔)
}

for

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

import "fmt"

func main() {
sum := 0
//for 初始化条件; 判断条件; 条件变化{
//}

//1)初始化条件 i:=0
//2)判断条件是否为真 i<=100 ,如果为真,执行循环体,如果为假,跳出循环
//3)条件变化 i++
//4)重复2,3
for i := 0; i < 100; i++ {
sum = sum + i
}
fmt.Println("sum=", sum)
}

range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func main() {
str := "abc"

//通过for打印每个字符
for i := 0; i < len(str); i++ {
fmt.Printf("str[%d]=%c\n", i, str[i])
}

//迭代打印每个元素,默认返回2个值:一个是元素的位置,一个是元素本身
for i, data := range str {
fmt.Printf("str[%d]=%c\n", i, data)
}

//第二个返回值,默认丢弃
//1)
for i := range str {
fmt.Printf("str[%d]=%c\n", i, str[i])
}

//2)
for i, _ := range str {
fmt.Printf("str[%d]=%c\n", i, str[i])
}
}

break,continue

image-20221212194635581

goto

image-20221212194735937

5.函数

定义格式

image-20221213141808884

无参无返回值

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

import "fmt"

// MyFunc 无参无返回值函数的定义
func MyFunc() {
a := 666
fmt.Println("a=", a)
}
func main() {
// 无参无返回值函数的调用:函数名()
MyFunc()
}

/* 写这里也行
func MyFunc() {
a := 666
fmt.Println("a=", a)
}
*/

有参无返回值

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"

//个人建议还是把每一个的参数类型写明吧

// MyFunc01 有参无返回值函数的定义
// 定义函数时,在函数名后面()定义的参数叫形参
// 参数传递,只能由实参传递给形参,不能反过来,单向传递
func MyFunc01(a int) {
a = 111
fmt.Println("a=", a)
}

func MyFunc02(a int, b int) {
a = 111
fmt.Println("a=", a, "b=", b)
}

func MyFunc03(a, b int) {
a = 111
fmt.Println("a=", a, "b=", b)
}

func MyFunc04(a int, b float64, c string) {

}

func MyFunc05(a, b float64, c string) {

}

func main() {
// 有参无返回值函数的调用:函数名(所需参数)
// 调用函数传递的参数叫实参
MyFunc01(121)
MyFunc03(11, 22)
}

不定参数类型

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

import "fmt"

func Myfunc01(a int, b int) { //固定参数

}

// Myfunc02 ...type 不定参数类型
// 注意:补丁参数,一定只能放在形参中的最后一个参数
// 固定参数一定要传参,不定参数根据需要传参
func Myfunc02(args ...int) {
fmt.Println("len(args)=", len(args))
for i := 0; i < len(args); i++ {
fmt.Printf("args[%d]=%d\n", i, args[i])
}

fmt.Println("__________")

for i, data := range args {
fmt.Printf("args[%d]=%d\n", i, data)
}
fmt.Println("__________")
}

func main() {
Myfunc01(1, 2)
Myfunc02()
Myfunc02(1)
Myfunc02(1, 2)
}

不定参数的传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func Myfunc01(args ...int) {
fmt.Println("Myfunc01")
for _, data := range args { //遍历参数列表
fmt.Println(data)
}
}

func Myfunc02(args ...int) {
fmt.Println("Myfunc02")
for _, data := range args { //遍历参数列表
fmt.Println(data)
}
}

func Test(args ...int) {
Myfunc01(args...)
Myfunc02(args[1:]...) //从下标是1的参数开始传递(包含1)
}

func main() {
Test(1, 2, 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
package main

import "fmt"

func Test01() int {
return 250
}

func Test02() (value int) {
value = 250
return value
}

func Test03() (value int) {
value = 250
return
}

func main() {
v1 := Test01()
v2 := Test02()
v3 := Test03()
fmt.Printf("V1=%d,V2=%d,V3=%d\n", v1, v2, v3)
}

多个返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func Test01() (int, string) {
return 250, "sb"
}

func Test02() (value int, strs string) {
value = 250
strs = "sb"
return value, strs
}

func Test03() (value int, strs string) {
value = 250
strs = "sb"
return
}

func main() {
v1, v2 := Test01()
_, v3 := Test02()
v4, v5 := Test03()
fmt.Printf("V1=%d,V2=%s,V3=%s,v4=%d,v5=%s\n", v1, v2, v3, v4, v5)
}

有参有返回值

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

import "fmt"

func max_element(num1 int, num2 int) (max int, min int) {
if num1 > num2 {
min = num2
max = num1
} else {
min = num1
max = num2
}
return max, min
}

func main() {
v1, v2 := max_element(10, 20)
fmt.Printf("v1_max=%d,v2_min=%d\n", v1, v2)
}

递归函数

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

import "fmt"

// 循环实现1+2+3+....100

func Test01() int {
sum := 0
for i := 1; i <= 100; i++ {
sum = sum + i
}
return sum
}

// 递归实现1+2+3+....100

func Test02(num int) int {
if num == 1 {
return 1
}
return num + Test02(num-1)
}

func main() {
result := Test01()
fmt.Printf("result=%d\n", result)
result2 := Test02(100)
fmt.Printf("result2=%d\n", result2)
}

函数类型

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 FuncType func(int, int) int //声明一个函数类型

func Test01(a, b int, f FuncType) (result int) {
result = f(a, b)
return result
}

func Add(a, b int) int {
return a + b
}

func Minus(a, b int) int {
return a - b
}

func main() {
v1 := Test01(1, 2, Add)
v2 := Test01(1, 2, Minus)
fmt.Printf("v1=%d,v2=%d\n", v1, v2)
}

函数有一个参数是函数类型,这个函数就是回调函数

匿名函数

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

import "fmt"

func main() {
a := 10
str := "mike"

//匿名函数,没有函数名字,函数定义,还没有调用
f1 := func() { //:=自动推导类型
fmt.Println("a=", a)
fmt.Println("str=", str)
}
f1()

//给一个函数类型起别名
type FuncType func() //函数没有参数和返回值
//声明变量
var f2 FuncType
f2 = f1
f2()

//定义匿名函数,同时调用
func() {
fmt.Println("a=", a)
fmt.Println("str=", str)
}()

//带参数的匿名函数
f3 := func(i, j int) {
fmt.Printf("i=%d,j=%d\n", i, j)
}
f3(1, 2)

//定义匿名函数,同时调用
func(i, j int) {
fmt.Printf("i=%d,j=%d\n", i, j)
}(4, 5)

//匿名函数,有参有返回值
x, y := func(i, j int) (max, min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return
}(10, 20)
fmt.Printf("x=%d,y=%d\n", x, y)
}

闭包

所谓闭包就是一个函数“捕获”了和它在同一作用域的其它常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。

在 Go 语言里,所有的匿名函数(Go 语言规范中称之为函数字面量)都是闭包。匿名函数是指不需要定义函数名的一种函数实现方式,它并不是一个新概念,最早可以回溯到 1958 年的Lisp 语言

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

import "fmt"

func main() {
a := 10
str := "mike"
func() {
//闭包是以引用方式捕获外部变量
a = 100
str = "go"
fmt.Println("a=", a)
fmt.Println("str=", str)
}()
fmt.Println("a=", a)
fmt.Println("str=", str)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func Test01() int {
//函数被调用时,x才分配空间,才初始化为0
var x int //没有初始化,值为0
x++
return x * x //函数调用完毕,x自动释放
}

func Test02() func() int {
var x int
return func() int { //匿名函数
x++ //捕获外部变量
return x * x
}
}

func main() {
fmt.Println(Test01())
fmt.Println(Test01())
fmt.Println(Test01())

f := Test02()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}

defer的作用

image-20221215202722509

多个defer执行顺序

image-20221215203014678

defer和匿名函数结合使用

image-20221215203433903

我的理解:a是值传递(实参已经先给匿名函数了),b不是值传递

获取命令行参数

image-20221215203842052

局部变量

image-20221215205057825

全局变量

image-20221215205731365

不同作用域同名变量

image-20221215205919330

6.工程管理

在实际的开发工作中,直接调用编译器进行编译和链接的场景是少而又少,因为在工程中不会简单到只有一个源代码文件,且源文件之间会有相互的依赖关系。如果这样一个文件一个文件逐步编译,那不亚于一场灾难。 Go 语言的设计者作为行业老将,自然不会忽略这一点。早期 Go 语言使用 makefile 作为临时方案,到了 Go 1 发布时引入了强大无比的 Go 命令行工具。

Go 命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推导工程结构和构建顺序。针对只有一个源文件的情况讨论工程管理看起来会比较多余,因为这可以直接用 go run 和 go build 搞定。下面我们将用一个更接近现实的虚拟项目来展示 Go语言的基本工程管理方法。

工作区

工作区介绍

Go 代码必须放在工作区中。工作区其实就是一个对应于特定工程的目录,它应包含 3 个子目录:src 目录、pkg 目录和 bin 目录。

  • src 目录:用于以代码包的形式组织并保存 Go 源码文件。(比如:.go .c .h .s 等)
  • pkg 目录:用于存放经由 go install 命令构建安装后的代码包(包含 Go 库源码文件)的“.a”归档文件。
  • bin 目录:与 pkg 目录类似,在通过 go install 命令完成安装后,保存由 Go 命令源码文件生成的可执行文件。

​ 目录 src 用于包含所有的源代码,是 Go 命令行工具一个强制的规则,而 pkg 和 bin 则无需手动创建,如果必要 Go 命令行工具在构建过程中会自动创建这些目录。

​ 需要特别注意的是,只有当环境变量 GOPATH 中只包含一个工作区的目录路径时,go install命令才会把命令源码安装到当前工作区的 bin 目录下。若环境变量 GOPATH 中包含多个工作区的目录路径,像这样执行 go install 命令就会失效,此时必须设置环境变量 GOBIN。

GOPATH

​ 为了能够构建这个工程,需要先把所需工程的根目录加入到环境变量 GOPATH 中。否则,即使处于同一工作目录(工作区),代码之间也无法通过绝对代码包路径完成调用。

​ 在实际开发环境中,工作目录往往有多个。这些工作目录的目录路径都需要添加至 GOPATH。当有多个目录时,请注意分隔符,多个目录的时候 Windows 是分号,Linux 系统是冒号,当有多个 GOPATH 时,默认会将 go get 的内容放在第一个目录下。

​ 所有 Go 语言的程序都会组织成若干组文件,每组文件被称为一个包。这样每个包的代码都可以作为很小的复用单元,被其他项目引用。

​ 一个包的源代码保存在一个或多个以.go 为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径

自定义包

image-20221215224334376

注意:同一个目录下不能定义不同的 package。

main包

​ 在 Go 语言里,命名为 main 的包具有特殊的含义。 Go 语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go 语言编译的可执行程序都必须有一个名叫 main的包。一个可执行程序有且仅有一个 main 包。

​ 当编译器发现某个包的名字为 main 时,它一定也会发现名为 main()的函数,否则不会创建可执行文件。 main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。程序编译时,会使用声明 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。

main函数和init函数

​ Go 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个 package里面可以写任意多个 init 函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个 package 中每个文件只写一个 init 函数。

​ Go 程序会自动调用 init()和 main(),所以你不需要在任何地方调用这两个函数。每个 package中的 init 函数都是可选的,但 package main 就必须包含一个 main 函数。

​ 每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的 init 函数都会安排在 main 函数之前执行。 init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。

​ 程序的初始化和执行都起始于 main 包。如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。

​ 有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt包,但它只会被导入一次,因为没有必要导入多次)。

​ 当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。下图详细地解释了整个执行过程:

image-20221215224716784

image-20221215224729605

image-20221215224737332

导入包

image-20221215224820397

image-20221215224833904

测试案例

image-20221215224918733

image-20221215224928810

image-20221215224948071

image-20221215225004996

image-20221215225016677

7.复合类型

分类

类型 名称 长度 默认值 说明
pointer 指针 nil
array 数组 0
slice 切片 nil 引用类型
map 字典 nil 引用类型
struct 结构体

指针

​ 指针是一个代表着某个内存地址的值。这个内存地址往往是在内存中存储的另一个变量的值的起始位置。Go 语言对指针的支持介于 Java 语言和 C/C++语言之间,它既没有想 Java 语言那样取消了代码对指针的直接操作的能力,也避免了 C/C++语言中由于对指针的滥用而造成的安全和可靠性问题。

基本操作

Go 语言虽然保留了指针,但与其它编程语言不同的是:

  • 默认值 nil,没有 NULL 常量
  • 操作符 “&” 取变量地址, “*” 通过指针访问目标对象
  • 不支持指针运算,不支持 “->” 运算符,直接⽤ “.” 访问目标成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var a int = 10 //声明一个变量,同时初识化
fmt.Printf("&a=%p\n", &a) //操作符“&” 取变量地址
var p *int = nil //声明一个变量p,类型为*int,指针类型
p = &a
fmt.Printf("p=%p\n", p)
fmt.Printf("a=%d,*p=%d\n", a, *p)

*p = 111 //*p操作指针所指向的内存,即为a
fmt.Printf("a=%d,*p=%d\n", a, *p)
}

new函数

​ 表达式 new(T)将创建一个 T 类型的匿名变量,所做的是为 T 类型的新值分配并清零一块内存空间,然后将这块内存空间的地址作为结果返回,而这个结果就是指向这个新的 T 类型值的指针值,返回的指针类型为*T。

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

import "fmt"

func main() {
var p *int
p = new(int) //P为*int类型,指向匿名的int变量
fmt.Println("p=", *p) //*p=0

p2 := new(int) //p2为*int类型,指向匿名的int变量
*p2 = 111
fmt.Println("p=", *p)
fmt.Println("p2=", *p2)
}

​ 我们只需使用 new()函数,无需担心其内存的生命周期或怎样将其删除,因为 Go 语言的内存管理系统会帮我们打理一切。

image-20221217141754378

指针做函数参数

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

import "fmt"

func swap(a, b int) {
a, b = b, a
fmt.Printf("a=%d,b=%d\n", a, b)
}

func swap02(a, b *int) {
*a, *b = *b, *a
}

func main() {
var a int = 10
var b int = 20
//swap(a, b)
swap02(&a, &b)
fmt.Printf("a=%d,b=%d\n", a, b)
}

数组

概述

image-20221217142558460

操作数组

数组的每个元素可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置。

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

import "fmt"

func main() {
var a [10]int
for i := 0; i < 10; i++ {
a[i] = i + 1
fmt.Printf("a[%d]=%d\n", i, a[i])
}
for i, data := range a {
fmt.Printf("a[%d]=%d\n", i, data)
}
}

内置函数 len(长度) 和 cap(容量) 都返回数组⻓度 (元素数量) ——数组长度固定所以值相同

1
2
a := [10]int{}
fmt.Println(len(a), cap(a)) //10 10

初始化:

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

func main() {
/*
未初始化元素值为 0
通过初始化值确定数组长度
通过索引号初始化元素,未初始化元素值为 0
*/
a := [3]int{1, 2}
for i, data := range a {
println("i=", i, "data=", data)
}
b := [...]int{1, 2, 3}
for i, data := range b {
println("i=", i, "data=", data)
}
c := [5]int{2: 100, 4: 100}
for i, data := range c {
println("i=", i, "data=", data)
}
}

image-20221217143946680

在函数传递数组

​ 根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。

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

import "fmt"

func modify(array [5]int) {
array[0] = 10 //试图修改数组的第一个元素
fmt.Println("In modify(),array values:", array)
}

func main() {
array := [5]int{1, 2, 3, 4, 5}
modify(array)
fmt.Println("In main(),array values:", array)
}

数组指针做函数参数

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

import "fmt"

func modify(array *[5]int) {
(*array)[0] = 10 //试图修改数组的第一个元素
fmt.Println("In modify(),array values:", *array)
}

func main() {
array := [5]int{1, 2, 3, 4, 5}
modify(&array)
fmt.Println("In main(),array values:", array)
}

冒泡排序

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

import "fmt"

func BubbleSort(arr []int)[]int{
for i := 0; i<len(arr) - 1; i++{
for j:= 0; j<len(arr)-1-i; j++{
if arr[j] > arr[j+1]{
arr[j],arr[j+1] = arr[j+1],arr[j]
}
}
}
return arr
}

func main(){
arr := []int{23,45,13,67,35}
fmt.Println(BubbleSort(arr))
}

随机数

1
2
3
4
5
6
7
func main() {
// 我们一般使用系统时间的不确定性来进行初始化
rand.Seed(time.Now().Unix())
for i:=0; i<10; i++ {
fmt.Print(rand.Intn(10), " ")
}
}

切片

概述

image-20221217145718001

切片的创建和初始化

slice 和数组的区别:声明数组时,方括号内写明了数组的长度或使用…自动计算长度,而声明 slice 时,方括号内没有任何字符。

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

func main() {
var s1 []int //声明切片和声明array一样,只是少了长度,此为空切片(nil)
s2 := []int{}
//make([]T,length,capacity) //capacity省略,则和length的值相同
var s3 []int = make([]int, 0)
s4 := make([]int, 0, 0)
s5 := []int{1, 2, 3}

}

注意:make 只能创建 slice、map 和 channel,并且返回一个有初始值(非零)。

切片的操作

image-20221219165855781

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

import "fmt"

func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] //[2 3 4]
s1[2] = 100 //修改切片某个元素改变底层数组
fmt.Println(s1, s) //[2 3 100] [0 1 2 3 100 5 6 7 8 9]

s2 := s1[2:6] //新切片依旧指向原底层数组 [100 5 6 7]
s2[3] = 200
fmt.Println(s2, s)
//[100 5 6 200] [0 1 2 3 100 5 6 200 8 9]
}

append 函数向 slice 尾部添加数据,返回新的 slice 对象:

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

import "fmt"

func main() {
var s1 []int //创建nil切换
//s1:=make([]int,0)
s1 = append(s1, 1)
s1 = append(s1, 2, 3)
s1 = append(s1, 4, 5, 6)
fmt.Println(s1)

s2 := make([]int, 5)
s2 = append(s2, 6)
fmt.Println(s2)

s3 := []int{1, 2, 3}
s3 = append(s3, 4, 5)
fmt.Println(s3)
//分别输出
//[1 2 3 4 5 6]
//[0 0 0 0 0 6]
//[1 2 3 4 5]
}

append 函数会智能地底层数组的容量增长,一旦超过原底层数组容量,通常以 2 倍容量重新分配底层数组,并复制原来的数据:

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

import "fmt"

func main() {
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap:%d--->%d\n", c, n)
c = n
}
}
}

函数 copy 在两个 slice 间复制数据,复制⻓度以 len 小的为准,两个 slice 可指向同⼀底层数组。

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

import "fmt"

func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:] //{8 9}
s2 := data[:5] //{0 1 2 3 4}
copy(s2, s1)

fmt.Println(s2) //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]
}

切片做函数参数

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

import "fmt"

func test(s []int) {
s[0] = -1
fmt.Println("test:")
for i, v := range s {
fmt.Printf("s[%d]=%d, ", i, v)
}
fmt.Println("\n")
}

func main() {
s1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
test(s1)
fmt.Println("main:")
for i, v := range s1 {
fmt.Printf("s[%d]=%d, ", i, v)
}
fmt.Println("\n")
}

map

概述

image-20221217220339652

map的创建

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

import "fmt"

func main() {
var m1 map[int]string //只是声明一个map,没有初始化,此为空map--nil
fmt.Println(m1 == nil) //true

//m2,m3的创建方法是等价的
m2 := map[int]string{}
m3 := make(map[int]string)
fmt.Println(m2, m3) //map[] map[]

m4 := make(map[int]string, 10) //第二个参数指定容量
fmt.Println(m4) //map[]
}

初始化

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

import "fmt"

func main() {
//定义并初始化
var m1 map[int]string = map[int]string{1: "mike", 2: "yoo"}
fmt.Println(m1)

//2.自动推导类型:=
m2 := map[int]string{1: "mike", 2: "yoo"}
fmt.Println(m2)
}

赋值

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

import "fmt"

func main() {
m1 := map[int]string{1: "mike", 2: "yoo"}
m1[1] = "xxx" //修改
m1[3] = "lily" //追加,go底层会自动为map分配空间
fmt.Println(m1) //map[1:xxx,2:yoo,3:lily]

m2 := make(map[int]string, 10) //创建map
m2[0] = "aaa"
m2[1] = "bbb"
fmt.Println(m2) //map[o:aaa,1:bbb]
fmt.Println(m2[0], m2[1]) //aaa bbb
}

遍历

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

import "fmt"

func main() {
m1 := map[int]string{1: "mike", 2: "yoo"}
//迭代遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d——>%s\n", k, v)
}

//迭代遍历2,第一个返回值是key,第二个返回值是value(可忽略)
for k := range m1 {
fmt.Printf("%d——>%s\n", k, m1[k])
}
//判断某个key所对应的value是否存在,第一个返回值为value(如果该值存在,不存在,value无值)
value, ok := m1[1]
fmt.Println(value, ok)

value2, ok2 := m1[3]
fmt.Println(value2, ok2)
}

删除

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

import "fmt"

func main() {
m1 := map[int]string{1: "mike", 2: "yoo"}
//迭代遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d——>%s\n", k, v)
}

delete(m1, 2) //删除m1中key值为2的那个
fmt.Println("_____")

for k, v := range m1 {
fmt.Printf("%d——>%s\n", k, v)
}
}

map做函数参数

在函数间传递映射并不会制造出该映射的一个副本,不是值传递,而是引用传递:

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

import "fmt"

func DeleteMap(m map[int]string, key int) {
delete(m, key)
for k, v := range m {
fmt.Printf("len(m)=%d,%d——>%s\n", len(m), k, v)
}
}

func main() {
m := map[int]string{1: "mike", 2: "yoo"}
DeleteMap(m, 2)
for k, v := range m {
fmt.Printf("len(m)=%d,%d——>%s\n", len(m), k, v)
}
}

结构体

结构体类型

image-20221219142934946

结构体初始化

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

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func main() {
//顺序初始化,每一个成员都要初始化
var s1 Student = Student{1, "mike", 'm', 18, "shanghai"}
fmt.Println(s1)
s2 := Student{2, "yoo", 'f', 20, "shanghai"}
fmt.Println(s2)
//指定初始化某个成员,没有初始化的成员为零值
s3 := Student{id: 2, name: "lily"}
fmt.Println(s3)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func main() {
//指针变量
var s5 *Student = &Student{3, "zhou", 'm', 18, "beijing"}
fmt.Println(*s5)
}

结构体成员的使用

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 Student struct {
id int
name string
sex byte
age int
address string
}

func main() {
var s1 Student = Student{1, "mike", 'm', 18, "shanghai"}
fmt.Printf("id=%d,name=%s,sex=%c,age=%d,address=%s\n", s1.id, s1.name, s1.sex, s1.age, s1.address)

var s2 Student
s2.id = 2
s2.name = "lily"
s2.sex = 'm'
s2.age = 18
s2.address = "shanghai"
fmt.Println(s2)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func main() {
//先分配空间,再赋值
s3 := new(Student)
s3.id = 3
s3.name = "xxx"
fmt.Println(s3, *s3)

//普通变量和指针变量类型打印
var s4 Student = Student{4, "yyy", 'm', 18, "shanghai"}
fmt.Println(s4, &s4)

//p.成员和(*p).成员操作是等价的
var p *Student = &s4
p.id = 5
(*p).name = "zzz"
fmt.Println(p, *p, s4)
}

结构体比较

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

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func main() {

var s4 Student = Student{4, "yyy", 'm', 18, "shanghai"}
var s5 Student = Student{4, "yyy", 'm', 18, "shanghai"}
fmt.Println("s4==s5", s4 == s5)
fmt.Println("s4!=s5", s4 != s5)
}

结构体作为函数参数

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

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func printStudentValue(tmp Student) {
tmp.id = 250
fmt.Println("printStudentValue tmp=", tmp)
}

func main() {
var s4 Student = Student{4, "yyy", 'm', 18, "shanghai"}
printStudentValue(s4) //值传递,形参修改不影响实参
fmt.Println("main s4=", s4)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type Student struct {
id int
name string
sex byte
age int
address string
}

func printStudentValue(tmp *Student) {
tmp.id = 250
fmt.Println("printStudentValue tmp=", tmp)
}

func main() {
var s4 Student = Student{4, "yyy", 'm', 18, "shanghai"}
printStudentValue(&s4) //引用传递,形参修改影响实参
fmt.Println("main s4=", s4)
}

可见性

image-20221219150118006

8.面向对象编程

概述

​ 对于面向对象编程的支持 Go 语言设计得非常简洁而优雅。因为, Go 语言并没有沿袭传统面向对象编程中的诸多概念,比如继承(不支持继承,尽管匿名字段的内存布局和行为类似继承,但它并不是继承)、虚函数、构造函数和析构函数、隐藏的 this 指针等。

尽管 Go 语言中没有封装、继承、多态这些概念,但同样通过别的方式实现这些特性:

  • 封装:通过方法实现
  • 继承:通过匿名字段实现
  • 多态:通过接口实现

匿名组合

​ 一般情况下,定义结构体的时候是字段名与其类型一一对应,实际上 Go 支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

​ 当匿名字段也是一个结构体的时候,那么这个结构体所拥有的全部字段都被隐式地引入了当前定义的这个结构体。

匿名字段,初始化

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

import "fmt"

// 人

type Person struct {
name string
sex byte
age int
}

// 学生

type Student struct {
Person //匿名字段,那么默认Student就包含了Person的所有字段
id int
address string
}

func main() {
//顺序初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "shanghai"}
fmt.Printf("s1=%v\n", s1)
// s2 := Student{"mike", 'm', 18, 1, "shanghai"}->error!

//部分成员初始化1
s3 := Student{Person: Person{"lily", 'f', 19}, id: 2}
fmt.Printf("s3=%v\n", s3)

//部分成员初始化2
s4 := Student{Person: Person{name: "Tom"}, id: 3}
fmt.Printf("s4=%v\n", s4)
}

成员的操作

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

import "fmt"

// 人
type Person struct {
name string
sex byte
age int
}

// 学生
type Student struct {
Person //匿名字段,那么默认Student就包含了Person的所有字段
id int
address string
}

func main() {
var s1 Student //变量声明
//给成员赋值
s1.name = "mike"
s1.sex = 'm'
s1.age = 18
s1.id = 1
s1.address = "shanghai"
fmt.Println(s1) //{{mike 109 18} 1 shanghai}

var s2 Student //变量声明
s2.Person = Person{"Lily", 'f', 19}
s2.id = 2
s2.address = "beijing"
fmt.Println(s2) // {{Lily 102 19} 2 beijing}
}

同名字段

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"

// 人

type Person struct {
name string
sex byte
age int
}

// 学生

type Student struct {
Person //匿名字段,那么默认Student就包含了Person的所有字段
id int
address string
name string
}

func main() {
var s1 Student //变量声明

//给Student的name,还是给Person赋值?
s1.name = "mike"
fmt.Printf("%v\n", s1)

//默认只会给最外层的成员赋值
//给匿名同名成员赋值,需要显示调用
s1.Person.name = "lily"
fmt.Printf("%v\n", s1)

//{{ 0 0} 0 mike}
//{{lily 0 0} 0 mike}
}

其他匿名字段

所有的内置类型和自定义类型都是可以作为匿名字段的:

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

import (
"fmt"
)

type mystr string //自定义类型

type Person struct {
name string
sex byte
age int
}

type Student struct {
Person //匿名字段,结构体类型
int //匿名字段,内置类型
mystr //匿名字段,自定义类型
}

func main() {
//初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "shanghai"}
fmt.Printf("%v\n", s1)

fmt.Printf("%s, %c,%d,%d,%s\n", s1.name, s1.sex, s1.age, s1.int, s1.mystr)
//{{mike 109 18} 1 shanghai}
//mike, m,18,1,shanghai
}

结构体指针

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

type Person struct {
name string
sex byte
age int
}

type Student struct {
*Person //匿名字段,结构体指针类型
id int
address string
}

func main() {
//初始化
s1 := Student{&Person{"mike", 'm', 18}, 1, "beijing"}
//{0xc0000543c0 1 beijing}
//mike,m,18
fmt.Printf("%v\n", s1)
fmt.Printf("%s,%c,%d\n", s1.name, s1.sex, s1.age)

//声明变量
var s2 Student
s2.Person = new(Person)
s2.name = "Lily"
s2.sex = 'f'
s2.age = 20
s2.id = 2
s2.address = "shanghai"

//Lily 102 20 2 shanghai
fmt.Println(s2.name, s2.sex, s2.age, s2.id, s2.address)
}

方法

概述

​ 在面向对象编程中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些函数,**这种带有接收者的函数,我们称为方法(method)**。 本质上,一个方法则是一个和特殊类型关联的函数。

​ 一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

​ 在 Go 语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。

⽅法总是绑定对象实例,并隐式将实例作为第⼀实参 (receiver),方法的语法如下:

1
func (receiver ReceiverType) funcName(parameters) (results)
  • 参数 receiver 可任意命名。如⽅法中未曾使⽤,可省略参数名。
  • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接⼝或指针。
  • 不支持重载方法,也就是说,不能定义名字相同但是不同参数的方法。

为类型添加方法

基础类型作为接收者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
)

type MyInt int //自定义类型,给int改名为MyInt

// 在函数定义时,在其名字之前放上一个变量,即是一个方法

func (a MyInt) Add(b MyInt) MyInt { //面向对象
return a + b
}

//传统方式的定义

func Add(a, b MyInt) MyInt { //面向过程
return a + b
}

func main() {
var a MyInt = 1
var b MyInt = 1

//调用func (a MyInt) Add(b MyInt)
fmt.Println("a.Add(b)=", a.Add(b)) //a.Add(b)=2

//调用func Add(a, b MyInt)
fmt.Println("Add(a,b)=", Add(a, b)) //Add(a,b)=2
}

通过上面的例子可以看出,面向对象只是换了一种语法形式来表达。方法是函数的语法糖,因为 receiver 其实就是方法所接收的第 1 个参数。

注意:虽然方法的名字一模一样,但是如果接收者不一样,那么方法就不一样。

结构体作为接收者

方法里面可以访问接收者的字段,调用方法通过点( . )访问,就像 struct 里面访问字段一样:

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

import "fmt"

type Person struct {
name string
sex byte
age int
}

func (p Person) PrintInfo() { //给Person添加方法
fmt.Println(p.name, p.sex, p.age)
}

func main() {
p := Person{"mike", 'm', 18} //初始化
p.PrintInfo() //调用func (p Person) PrintInfo()
}
值语义和引用语义
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
package main

import "fmt"

type Person struct {
name string
sex byte
age int
}

func (p *Person) SetInfoPointer() { //指针作为接收者,引用语义
//给成员赋值
(*p).name = "Lily"
p.sex = 'f'
p.age = 22
}

func (p Person) SetInfoValue() { //值作为接收者,值语义
//给成员赋值
p.name = "Lily"
p.sex = 'f'
p.age = 22
}

func main() {
//指针作为接收者,引用语义
p1 := Person{"mike", 'm', 18} //初始化
fmt.Println("函数调用前=", p1) //函数调用前=
(&p1).SetInfoPointer()
fmt.Println("函数调用后=", p1) //函数调用后=

fmt.Println("---------------")

p2 := Person{"mike", 'm', 18} //初始化
//值作为接收者,值语义
fmt.Println("函数调用前=", p2) //函数调用前=
p2.SetInfoValue()
fmt.Println("函数调用后=", p2) //函数调用后=


/*
函数调用前= {mike 109 18}
函数调用后= {Lily 102 22}
---------------
函数调用前= {mike 109 18}
函数调用后= {mike 109 18}
*/

}

方法集

​ 类型的方法集是指可以被该类型的值调用的所有方法的集合。

​ 用实例实例 value 和 pointer 调用方法(含匿名字段)不受⽅法集约束,编译器编总是查找全部方法,并自动转换 receiver 实参。

类型*T 方法集

​ 一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。

​ 如果在指针上调用一个接受值的方法,Go 语言会聪明地将该指针解引用,并将指针所指的底层值作为方法的接收者。

类型 *T ⽅法集包含全部 receiver T + *T ⽅法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

type Person struct {
name string
sex byte
age int
}

func (p *Person) SetInfoPointer() { //指针作为接收者,引用语义
//给成员赋值
(*p).name = "Lily"
p.sex = 'f'
p.age = 22
}

func (p Person) SetInfoValue() { //值作为接收者,值语义
//给成员赋值
p.name = "xxx"
p.sex = 'm'
p.age = 33
}

func main() {
var p *Person = &Person{"mike", 'm', 18}
p.SetInfoPointer()
p.SetInfoValue()
(*p).SetInfoValue()
}
类型T 方法集

​ 一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成,但是不包含那些接收者类型为指针的方法。

​ 但这种限制通常并不像这里所说的那样,因为如果我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这可以借助于 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
package main

type Person struct {
name string
sex byte
age int
}

func (p *Person) SetInfoPointer() { //指针作为接收者,引用语义
//给成员赋值
(*p).name = "Lily"
p.sex = 'f'
p.age = 22
}

func (p Person) SetInfoValue() { //值作为接收者,值语义
//给成员赋值
p.name = "xxx"
p.sex = 'm'
p.age = 33
}

func main() {
var p Person = Person{"mike", 'm', 18}
(&p).SetInfoPointer()
p.SetInfoPointer()

p.SetInfoValue()
(&p).SetInfoValue()
}

还是看是什么语义,引用语义还是会使主函数的值变化

匿名字段

方法的继承

如果匿名字段实现了一个方法,那么包含这个匿名字段的 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
package main

import "fmt"

type Person struct {
name string
sex byte
age int
}

//Person定义了方法

func (p *Person) PrintInfo() { //给Person添加方法
fmt.Println(p.name, p.sex, p.age)
}

type Student struct {
Person //匿名字段,那么Student包含了Person的所有字段
id int
address string
}

func main() {
p := Person{"mike", 'm', 18}
p.PrintInfo()
s := Student{Person{"Lily", 'f', 20}, 2, "shanghai"}
s.PrintInfo()
}
方法的重写
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"

type Person struct {
name string
sex byte
age int
}

//Person定义了方法

func (p *Person) PrintInfo() { //给Person添加方法
fmt.Println("Person:", p.name, p.sex, p.age)
}

type Student struct {
Person //匿名字段,那么Student包含了Person的所有字段
id int
address string
}

//Student定义了方法

func (s *Student) PrintInfo() { //给Person添加方法
fmt.Println("Student:", s.name, s.sex, s.age)
}

func main() {
p := Person{"mike", 'm', 18}
p.PrintInfo()
s := Student{Person{"Lily", 'f', 20}, 2, "shanghai"}
s.PrintInfo()
s.Person.PrintInfo()

//Person: mike 109 18
//Student: Lily 102 20
//Person: Lily 102 20
}

表达式

​ 类似于我们可以对函数进行赋值和传递一样,方法也可以进行赋值和传递。

​ 根据调用者不同,方法分为两种表现形式:方法值和方法表达式。两者都可像普通函数那样赋值和传参,区别在于方法值绑定实例,⽽方法表达式则须显式传参。

方法值

image-20221221203817345

方法表达式

image-20221221203820308

接口

概述

​ 在Go 语言中,接口(interface)是一个自定义类型,接口类型具体描述了一系列方法的集合。

​ 接口类型是一种抽象的类型,它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它们只会展示出它们自己的方法。因此接口类型不能将其实例化。

​ Go 通过接口实现了鸭子类型(duck-typing):“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。

接口的使用

接口定义

image-20221221210856274

接口实现

​ 接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现,一个实现了这些方法的具体类型是这个接口类型的实例。

​ 如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

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

import "fmt"

type Humaner interface {
SayHi()
}

type Student struct { //学生
name string
score float64
}

func (s *Student) SayHi() { // Student实现SayHi()方法
fmt.Printf("Stduent[%s,%f] say hi!!\n", s.name, s.score)
}

type Teacher struct { //老师
name string
group string
}

func (t *Teacher) SayHi() { // Teacher实现SayHi()方法
fmt.Printf("Teacher[%s,%s] say hi!!\n", t.name, t.group)
}

type Mystr string

func (str Mystr) SayHi() { //Mystr实现SayHi()方法
fmt.Printf("Mystr[%s] say hi!!\n", str)
}

func WhoSayHi(i Humaner) { //普通函数,参数为Humaner类型的变量i
i.SayHi()
}

func main() {
s := &Student{"mike", 88.88}
t := &Teacher{"Lily", "GO语言"}
var temp Mystr = "测试"

s.SayHi()
t.SayHi()
temp.SayHi()
/*
Stduent[mike,88.880000] say hi!!
Teacher[Lily,GO语言] say hi!!
Mystr[测试] say hi!!
*/

//多态,调用同一接口,不同表现
WhoSayHi(s)
WhoSayHi(t)
WhoSayHi(temp)
/*
Stduent[mike,88.880000] say hi!!
Teacher[Lily,GO语言] say hi!!
Mystr[测试] say hi!!
*/

x := make([]Humaner, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = s, t, temp
for _, value := range x {
value.SayHi()
}
/*
Stduent[mike,88.880000] say hi!!
Teacher[Lily,GO语言] say hi!!
Mystr[测试] say hi!!
*/
}

​ 通过上面的代码,你会发现接口就是一组抽象方法的集合,它必须由其他非接口类型实现,而不能自我实现。

接口组合

​ 接口嵌入:如果一个 interface1 作为 interface2 的一个嵌入字段,那么 interface2 隐式的包含了 interface1里面的方法。

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"

type Humaner interface {
SayHi()
}

type Personer interface {
Humaner //这里想写SayHi()一样
Sing(lyrics string)
}

type Student struct { //学生
name string
score float64
}

func (s *Student) SayHi() { // Student实现SayHi()方法
fmt.Printf("Stduent[%s,%f] say hi!!\n", s.name, s.score)
}

func (s *Student) Sing(lyrics string) { // Student实现Sing()方法
fmt.Printf("Stduent sing[%s]!!\n", lyrics)
}

func main() {
s := &Student{"mike", 88.88}
var i2 Personer
i2 = s
i2.SayHi()
i2.Sing("小帅哥")
//Stduent[mike,88.880000] say hi!!
//Stduent sing[小帅哥]!!
}

接口转化:超级接口对象可转换为子集接口,反之出错:

image-20221221230709600

image-20221221230715889

空接口

image-20221221230846809

类型查询

我们知道 interface 的变量里面可以存储任意类型的数值(该类型实现了 interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

  • comma-ok 断言
  • switch 测试

comma-ok 断言:

​ Go 语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里 value 就是变量的值,ok 是一个 bool 类型,element 是 interface 变量,T 是断言的类型。如果 element 里面确实存储了 T 类型的数值,那么 ok 返回 true,否则返回 false。

image-20221221234430828

switch 测试:

image-20221221234512226

9.异常处理

error接口

Go 语言引入了一个关于错误处理的标准模式,即 error 接口,它是 Go 语言内建的接口类型,该接口的定义如下:

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

Go 语言的标准库代码包 errors 为用户提供如下方法:

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

type errorString struct {
text string
}

func New(text string) error {
return &errorString{text}
}

func (e *errorString) Error() string {
return e.text
}

另一个可以生成 error 类型值的方法是调用 fmt 包中的 Errorf 函数:

1
2
3
4
5
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}

示例代码:

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

import (
"errors"
"fmt"
)

func main(){
var err1 error=errors.New("a normal err1")
fmt.Println(err1)

var err2 error=fmt.Errorf("%s","a normal err2")
fmt.Println(err2)
}

函数通常在最后的返回值中返回错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"errors"
"fmt"
)

func Divide(a,b float64) (result float64,err error){
if b==0{
result=0.0
err=errors.New("runtime error:divide by zero")
return
}
result =a/b
err=nil
return
}


func main(){
r ,err:=Divide(10.0,1)
if err!=nil{
fmt.Println(err) //错误处理 "runtime error:divide by zero"
} else{
fmt.Println(r) //使用返回值
}
}

panic

​ 在通常情况下,向程序使用方报告错误状态的方式可以是返回一个额外的 error 类型值。

​ 但是,当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起 painc 异常。这时,上述错误处理方式显然就不适合了。反过来讲,在一般情况下,我们不应通过调用 panic 函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用 panic。

​ 一般而言,当 panic 异常发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程,在中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息。

​ 不是所有的 panic 异常都来自运行时,直接调用内置的 panic 函数也会引发 panic 异常;panic函数接受任何值作为参数。

1
func panic(v interface{})

调用 panic 函数引发的 panic 异常:

image-20221222104213660

内置的 panic 函数引发的 panic 异常:

image-20221222104348853

recover

​ 运行时 panic 异常一旦被引发就会导致程序崩溃。这当然不是我们愿意看到的,因为谁也不能保证程序不会发生任何运行时错误。

​ 不过,Go 语言为我们提供了专用于“拦截”运行时 panic 的内建函数——recover。它可以是当前的程序从运行时 panic 的状态中恢复并重新获得流程控制权。

1
func recover() interface{}

注意:recover 只有在 defer 调用的函数中有效。

​ 如果调用了内置函数 recover,并且定义该 defer 语句的函数发生了 panic 异常,recover 会使程序从 panic 中恢复,并返回 panic value。导致 panic 异常的函数不会继续运行,但能正常返回。在未发生 panic 时调用 recover,recover 会返回 nil。

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

import "fmt"

func TestA() {
fmt.Println("func TestA()")
}

func TestB() (err error) {
defer func() { //在发生异常时,设置恢复
if x := recover(); x != nil {
//panic value 被附加到错误信息中;
//并用 err 变量接收错误信息,返回给调用者。
err = fmt.Errorf("internal error: %v", x)
}
}()
panic("func TestB(): panic")
}

func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
err := TestB()
fmt.Println(err)
TestC()
/*
运行结果:
func TestA()
internal error: func TestB(): panic
func TestC()
*/
}

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后⼀个错误可被捕获

image-20221222105655519

10.文本文件处理

字符串处理

​ 字符串在开发中经常用到,包括用户的输入,数据库读取的数据等,我们经常需要对字符串进行分割、连接、转换等操作,我们可以通过 Go 标准库中的 strings 和 strconv 两个包中的函数进行相应的操作。

字符串操作

下面这些函数来自于 strings 包,这里介绍一些我平常经常用到的函数,更详细的请参考官方的文档。

Contains
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
func Contains(s, substr string) bool
功能:字符串 s 中是否包含 substr,返回 bool 值
*/

package main

import (
"fmt"
"strings"
)

func main() {
fmt.Println(strings.Contains("seafood","foo"))
fmt.Println(strings.Contains("seafood","bar"))
fmt.Println(strings.Contains("seafood",""))
fmt.Println(strings.Contains("",""))
//true
//false
//true
//true
}
Join
1
2
3
4
5
6
//func Join(a []string, sep string) string
//功能:字符串链接,把 slice a 通过 sep 链接起来

s:=[]string{"foo","bar","baz"}
fmt.Println(strings.Join(s,","))
//foo,bar,baz
Index
1
2
3
4
5
6
//func Index(s, sep string) int
//功能:在字符串 s 中查找 sep 所在的位置,返回位置值,找不到返回-1
fmt.Println(strings.Index("chicken","ken"))
fmt.Println(strings.Index("chicken","dmr"))
//4
//-1
Repeat
1
2
3
4
//func Repeat(s string, count int) string
//功能:重复 s 字符串 count 次,最后返回重复的字符串
fmt.Println(strings.Repeat("na",2))
//nana
Replace
1
2
3
4
5
6
//func Replace(s, old, new string, n int) string
//功能:在 s 字符串中,把 old 字符串替换为 new 字符串,n 表示替换的次数,小于 0 表示全部替换
fmt.Println(strings.Replace("oink oink oink","k","ky",2))
fmt.Println(strings.Replace("oink oink oink","oink","moo",-1))
//oinky oinky oink
//moo moo moo
splict

image-20221222115337018

Trim

image-20221222115409755

Fields

image-20221222115424666

字符串转化

append

image-20221222120446823

format

image-20221222120501046

parse

image-20221222120524867

正则表达式

https://github.com/google/re2

https://studygolang.com/articles/28796

JSON处理

​ JSON (JavaScript Object Notation)是一种比 XML 更轻量级的数据交换格式,在易于人们阅读和编写的同时,也易于程序解析和生成。尽管 JSON 是 JavaScript 的一个子集,但 JSON采用完全独立于编程语言的文本格式,且表现为键/值对集合的文本描述形式(类似一些编程语言中的字典结构),这使它成为较为理想的、跨平台、跨语言的数据交换语言。

image-20221222130803675

​ 开发者可以用 JSON 传输简单的字符串、数字、布尔值,也可以传输一个数组,或者一个更复杂的复合结构。在 Web 开发领域中, JSON 被广泛应用于 Web 服务端程序和客户端之间的数据通信。

​ Go 语言内建对 JSON 的支持。使用 Go 语言内置的 encoding/json 标准库,开发者可以轻松使用 Go 程序生成和解析 JSON 格式的数据。

JSON 官方网站:http://www.json.org/

在线格式化:http://www.json.cn/

编码json

通过结构体生成json

image-20221222133407516

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

import (
"encoding/json"
"fmt"
)

type IT struct{
Company string
Subjects []string
IsOk bool
Price float64
}


func main() {
t1:=IT{"itcast",[]string{"GO","C++","Python","Test"},true,666.666}
/*
生成一段 JSON 格式的文本
如果编码成功, err 将赋于零值 nil,变量 b 将会是一个进行 JSON 格式化之后的
[]byte 类型
b, err := json.Marshal(t1)
输出结果:
{"Company":"itcast","Subjects":["Go","C++","Python","Test"],"IsOk":tr
ue,"Price":666.666}
*/
b,err:=json.MarshalIndent(t1,""," ")
if err!=nil{
fmt.Println("json err:",err)
}
fmt.Println(string(b))

/*
{
"Company": "itcast",
"Subjects": [
"GO",
"C++",
"Python",
"Test"
],
"IsOk": true,
"Price": 666.666
}
*/
}

我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON 输出的时候必须注意,只有导出的字段(首字母是大写)才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过 struct tag定义来实现。

针对 JSON 的输出,我们在定义 struct tag 的时候需要注意的几点是:

  • 字段的 tag 是”-“,那么这个字段不会输出到 JSON
  • tag 中带有自定义名称,那么这个自定义名称会出现在 JSON 的字段名中
  • tag 中如果带有”omitempty”选项,那么如果该字段值为空,就不会输出到 JSON 串中
  • 如果字段类型是 bool, string, int, int64 等,而 tag 中带有”,string”选项,那么这个字段在输出到 JSON 的时候会把该字段对应的值转换成 JSON 字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"encoding/json"
"fmt"
)

type IT struct {
//Company 不会导出到 JSON 中
Company string `json:"-"`
// Subjects 的值会进行二次 JSON 编码
Subjects []string `json:"subjects"`1
//转换为字符串,再输出
IsOk bool `json:",string"`
// 如果 Price 为空,则不输出到 JSON 串中
Price float64 `json:"price,omitempty"`
}
func main() {
t1 := IT{Company: "itcast", Subjects: []string{"Go", "C++", "Python","Test"}, IsOk: true}
b, err := json.Marshal(t1)
//json.MarshalIndent(t1, "", " ")
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
//输出结果:
//{"subjects":["Go","C++","Python","Test"],"IsOk":"true","price":0}
}
通过map生成JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"encoding/json"
"fmt"
)


func main() {
// 创建一个保存键值对的映射
t1 := make(map[string]interface{})
t1["company"] = "itcast"
t1["subjects "] = []string{"Go", "C++", "Python", "Test"}
t1["isok"] = true
t1["price"] = 666.666
b, err := json.Marshal(t1)
//json.MarshalIndent(t1, "", " ")

if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
//输出结果:
//{"company":"itcast","isok":true,"price":666.666,"subjects
// ":["Go","C++","Python","Test"]}
}

解码Json

image-20221222142929599

解析到结构体
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
package main

import (
"encoding/json"
"fmt"
)

type IT struct {
Company string `json:"company"`
Subjects []string `json:"subjects"`
IsOk bool `json:"isok"`
Price float64 `json:"price"`
}
func main() {
b := []byte(`{
"company": "itcast",
"subjects": [
"Go",
"C++",
"Python",
"Test"
],
"isok": true,
"price": 666.666
}`)
var t IT
err := json.Unmarshal(b, &t)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(t)
//运行结果:{itcast [Go C++ Python Test] true 666.666}
//只想要 Subjects 字段
type IT2 struct {
Subjects []string `json:"subjects"`
}
var t2 IT2
err = json.Unmarshal(b, &t2)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(t2)
//运行结果:{[Go C++ Python Test]}
}
解析到interface
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
package main

import (
"encoding/json"
"fmt"
)

type IT struct {
Company string `json:"company"`
Subjects []string `json:"subjects"`
Isok bool `json:"isok"`
Price float64 `json:"price"`
}

func main() {
b:=[]byte(`{
"company": "itcast",
"subjects": [
"GO",
"C++",
"Python",
"Test"
],
"isok": true,
"price": 666.666
}`)

var t interface{}
err:=json.Unmarshal(b,&t)
if err!=nil{
fmt.Println("json err:",err)
}
fmt.Println(t)
//使用断言判断类型 element.(type)
m:=t.(map[string]interface{})

for k,v :=range m{
switch vv:=v.(type) {
case string:
fmt.Println(k, "is string", vv)
case int:
fmt.Println(k, "is int", vv)
case float64:
fmt.Println(k, "is float64", vv)
case bool:
fmt.Println(k, "is bool", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
}

文件操作

image-20221222150459905

image-20221222150509424

写文件

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

import (
"fmt"
"os"
)

func main(){
fout,err:=os.Create("./xxx.txt") //新建文件

if err!=nil{
fmt.Println(err)
return
}
defer fout.Close() //main函数结束前,关闭文件

for i:=0;i<5;i++{
outstr:=fmt.Sprintf("%s:%d\n","hello go",i)
fout.WriteString(outstr) //写入string信息到文件
fout.Write([]byte("abcd\n")) //写入byte类型的信息到文件。

}
}

读文件

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

import (
"fmt"
"os"
)

func main(){
fin,err:=os.Open("./xxx.txt") //打开文件
if err!=nil{
fmt.Println(err)
}
defer fin.Close()
buf:=make([]byte,1024) //开辟1024个字节的slice作为缓冲
for{
n,_ :=fin.Read(buf) //读文件
if n==0{ //0表示已经到文件结束
break
}
}
fmt.Println(string(buf)) //输出读取的内容
}

拷贝文件

image-20221222154233047

image-20221222154241644

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

import (
"fmt"
"io"
"os"
)

func main() {
//复制前的文件路径,复制完的文件路径。
srcPath := "zx.txt"
dstPath := "test.txt"
srcFile, err1 := os.Open(srcPath) //打开
if err1 != nil {
fmt.Println(err1)
return
}
dstFile, err2 := os.Create(dstPath)
if err2 != nil {
fmt.Println(err2)
return
}
buf := make([]byte, 1024)

for {
n, err := srcFile.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
break
}
if n == 0 {
break
}
tmp := buf[:n]
dstFile.Write(tmp)
}

srcFile.Close()
dstFile.Close()
}

11.并发编程

概述

并行和并发

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。

image-20221223122000183

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

image-20221223122029893

  • 并行是两个队列同时使用两台咖啡机
  • 并发是两个队列交替使用一台咖啡机

image-20221223122054628

go语言并发优势

​ 有人把 Go 比作 21 世纪的 C 语言,第一是因为 Go 语言设计简单,第二,21 世纪最重要的就是并行程序设计,而 Go 从语言层面就支持了并行。同时,并发程序的内存管理有时候是非常复杂的,而 Go 语言提供了自动垃圾回收机制。

​ Go 语言为并发编程而内置的上层 API 基于 CSP(communicating sequential processes, 顺序通信进程)模型。这就意味着显式锁都是可以避免的,因为 Go 语言通过相册安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。

​ 一般情况下,一个普通的桌面计算机跑十几二十个线程就有点负载过大了,但是同样这台机

器却可以轻松地让成百上千甚至过万个 goroutine 进行资源竞争。

goroutine

goroutine是什么

​ goroutine 是 Go 并行设计的核心。goroutine 说到底其实就是协程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,Go 语言内部帮你实现了这些 goroutine 之间的内存共享。执行 goroutine 只需极少的栈内存(大概是 4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine 比 thread 更易用、更高效、更轻便。

创建goroutine

​ 只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

​ 在并发编程里,我们通常想讲一个过程切分成几块,然后让每个 goroutine 各自负责一块工作。当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫它 main goroutine。新的 goroutine 会用 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
package main

import (
"fmt"
"time"
)

func NewTask() {
i := 0
for {
i++
fmt.Printf("new goroutine:i=%d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}

func main() {
//创建一个goroutine,启动另外一个任务
go NewTask()

i := 0
//main goroutine 循环打印
for {
i++
fmt.Printf("main goroutine:i=%d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}

image-20221223124306385

主goroutine先退出

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

import (
"fmt"
"time"
)

func NewTask() {
i := 0
for {
i++
fmt.Printf("new goroutine:i=%d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}

func main() {
//创建一个goroutine,启动另外一个任务
go NewTask()

fmt.Println("main goroutine exit")
}

main goroutine退出,其他goroutine全部退出。

runtime包

Gosched

​ runtime.Gosched() 用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

​ 这就像跑接力赛,A 跑了一会碰到代码 runtime.Gosched() 就把接力棒交给 B 了,A 歇着了,B 继续跑 。

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

import (
"fmt"
"runtime"
)

func main() {
//创建一个goroutine
go func(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}("world")

for i := 0; i < 2; i++ {
runtime.Gosched() //import "runtime"

fmt.Println("hello")
}

}

image-20221223130100705

Goexit

调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。

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

import (
"fmt"
"runtime"
)

func main() {
go func() {
defer fmt.Println("a.defer")

func() {
defer fmt.Println("b.defer")
runtime.Goexit() //终止当前goroutine
fmt.Println("b")
}()
fmt.Println("a") //不会执行
}()
//死循环,目的不让主goroutine结束
for {

}
}

image-20221223133211856

GOMAXPROCS

调用 runtime.GOMAXPROCS() 用来设置可以并行计算的 CPU 核数的最大值,并返回之前的值。

image-20221223133630391

​ 在第一次执行(runtime.GOMAXPROCS(1))时,最多同时只能有一个 goroutine 被执行。所以会打印很多 1。过了一段时间后,GO 调度器会将其置为休眠,并唤醒另一个 goroutine,这时候就开始打印很多 0 了,在打印的时候,goroutine 是被调度到操作系统线程上的。

​ 在第二次执行(runtime.GOMAXPROCS(2))时,我们使用了两个 CPU,所以两个 goroutine 可以一起被执行,以同样的频率交替打印 0 和 1。

channel

​ goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

​ 引⽤类型 channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

channel类型

​ 和 map 类似,channel 也一个对应 make 创建的底层数据结构的引用。

​ 当我们复制一个 channel 或用于函数参数传递时,我们只是拷贝了一个 channel 引用,因此调用者何被调用者将引用同一个 channel 对象。和其它的引用类型一样,channel 的零值也是nil。

​ 定义一个 channel 时,也需要定义发送到 channel 的值的类型。channel 可以使用内置的 make()函数来创建:

1
2
make(chan Type) //等价于 make(chan Type, 0)
make(chan Type, capacity)

​ 当 capacity= 0 时,channel 是无缓冲阻塞读写的,当 capacity> 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity 个元素才阻塞写入。

​ channel 通过操作符<-来接收和发送数据,发送和接收数据语法:

1
2
3
4
channel <- value //发送 value 到 channel
<-channel //接收并将其丢弃
x := <-channel //从 channel 中接收数据,并赋值给 x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空

​ 默认情况下,channel 接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine 同步变的更加的简单,而不需要显式的 lock。

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

import "fmt"

func main() {
c := make(chan int)

go func() {
defer fmt.Println("子协程结束")
fmt.Println("子协程正在运行...")
c <- 666 //666发送到c
}()
num := <-c //从c中接受数据,并赋值给num

fmt.Println("num=", num)
fmt.Println("main协程结束")
}

无缓冲的channel

​ 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。

​ 这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

​ 这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:

image-20221223141533675

  • 在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。
  • 在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成。
  • 在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个goroutine 一样也会在通道中被锁住,直到交换完成。
  • 在第 4 步和第 5 步,进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。

无缓冲的 channel 创建格式:

make(chan Type) //等价于 make(chan Type, 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
25
26
27
28
package main

import (
"fmt"
"time"
)

func main() {
c := make(chan int, 0) //无缓冲的通道

//内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小
fmt.Printf("len(c)=%d,cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子协程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c), cap(c))
}
}()

time.Sleep(2 * time.Second) //延时2s

for i := 0; i < 3; i++ {
num := <-c //从从中接收数据,并赋值给num
fmt.Println("num=", num)
}
fmt.Println("main协程结束")
}

有缓冲的channel

​ 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。

​ 这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

​ 这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

image-20221223145828578

  • 在第 1 步,右侧的 goroutine 正在从通道接收一个值。
  • 在第 2 步,右侧的这个 goroutine 独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。
  • 在第 3 步,左侧的 goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
  • 最后,在第 4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。

有缓冲的 channel 创建格式:

make(**chan** Type, capacity)

​ 如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

func main() {
c := make(chan int, 3) //有缓冲的通道

//内置函数len返回未被读取的缓冲元素数量,cap返回缓冲区大小
fmt.Printf("len(c)=%d,cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子协程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子协程正在运行[%d]:len(c)=%d,cap(c)=%d\n", i, len(c), cap(c))
}
}()

time.Sleep(2 * time.Second) //延时2s

for i := 0; i < 3; i++ {
num := <-c //从从中接收数据,并赋值给num
fmt.Println("num=", num)
}
fmt.Println("main协程结束")
}

range和close

​ 如果发送者知道,没有更多的值需要发送到 channel 的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close 函数来关闭 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
package main

import "fmt"

func main() {
c := make(chan int)

go func() {
for i := 0; i < 5; i++ {
c <- i
}
// 把close(c)注释掉,程序会一直阻塞在if data, ok := <-c; ok 那一行
close(c)
}()

for {
//ok为true说明channel没有关闭,为false说明管道已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
  • channel 不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束 range 循环之类的,才去关闭 channel;
  • 关闭 channel 后,无法向 channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭 channel 后,可以继续向 channel 接收数据;
  • 对于 nil channel,无论收发都会被阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
c := make(chan int)

go func() {
for i := 0; i < 5; i++ {
c <- i
}
// 把close(c)注释掉,程序会一直阻塞在if data, ok := <-c; ok 那一行
close(c)
}()

for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}

单向的channel

​ 默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。

​ 但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

​ 单向 channel 变量的声明非常简单,如下:

1
2
3
4
5
var ch1 chan int // ch1 是一个正常的 channel,不是单向的 

var ch2 chan<- float64 // ch2 是单向 channel,只用于写 float64 数据

var ch3 <-chan int // ch3 是单向 channel,只用于读取 int 数据
  • chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
  • <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。

可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通channel:

image-20221223154404073

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
)

func counter(out chan<- int) { // chan<-只写
defer close(out)
for i := 0; i < 5; i++ {
out <- i //如果对方不读,会阻塞
}
}

func printer(in <-chan int) { // <-chan只读
for num := range in {
fmt.Println(num)
}
}

func main() {
c := make(chan int) //chan 读写

//生产者,消费者
go counter(c)
printer(c)
fmt.Println("done")
}

定时器

Timer

Timer 是一个定时器,代表未来的一个单一事件,你可以告诉 timer 你要等待多长时间,它提供一个 channel,在将来的那个时间那个 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"time"
)

func main() {
//创建定时器,2秒后,定时器就会向自己的c字节发送一个time.Time类型的元素值
timer := time.NewTimer(time.Second * 2)
t1 := time.Now() //当前时间
fmt.Printf("t1:%v\n", t1)

t2 := <-timer.C
fmt.Printf("t2:%v\n", t2)

//如果只是单纯的等待的话,可以使用time.sleep来实现
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s后")

time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")

<-time.After(time.Second * 2)
fmt.Println("再一次2s后")

timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()

stop := timer3.Stop() //停止定时器
if stop {
fmt.Println("Timer 3 stopped")
}

fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原来设置3s
timer4.Reset(time.Second * 1) //重新设置时间
<-timer4.C
fmt.Println("after")
}

Ticker

​ Ticker 是一个定时触发的计时器,它会以一个间隔(interval)往 channel 发送一个事件(当前时间),而 channel 的接收者可以以固定的时间间隔从 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
package main

import (
"fmt"
"time"
)

func main() {
//创建定时器,每隔 1 秒后,定时器就会给 channel 发送一个事件(当前时间)
ticker := time.NewTicker(time.Second * 1)
i := 0
go func() {
for { //循环
<-ticker.C
i++
fmt.Println("i = ", i)
if i == 5 {
ticker.Stop() //停止定时器
}
}
}() //别忘了()
//死循环,特地不让 main goroutine 结束

for {
}
}

select

​ Go 里面提供了一个关键字 select,通过 select 可以监听 channel 上的数据流动。select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case语句来描述。

​ 与 switch 语句可以选择任何可使用相等比较的条件相比, select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作,大致的结构如下:

1
2
3
4
5
6
7
8
select {
case <-chan1:
// 如果 chan1 成功读到数据,则进行该 case 处理语句
case chan2 <- 1:
// 如果成功向 chan2 写入数据,则进行该 case 处理语句
default:
// 如果上面都没有成功,则进入 default 处理流程
}

​ 在一个 select 语句中,Go 语言会按顺序从头至尾评估每一个发送和接收的语句。

​ 如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

  • 如果给出了 default 语句,那么就会执行 default 语句,同时程序的执行会从 select 语句后的语句中恢复。
  • 如果没有 default 语句,那么 select 语句将被阻塞,直到至少有一个通信可以进行下去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")

return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

​ 有时候会出现 goroutine 阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用 select 来设置超时,通过如下的方式实现:

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

func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
o <- true
break

}
}
}()
//c <- 666 // 注释掉,引发 timeout
<-o
}

12.网络编程

网络概述

image-20221223184924373

image-20221223184938057

image-20221223185153395

image-20221223185217341

image-20221223185227028

image-20221223185234563

socket编程

什么是socket

​ Socket 起源于 Unix,而 Unix 基本哲学之一就是“一切皆文件”,都可以用“打开 open –> 读写 write/read –> 关闭 close”模式来操作。Socket 就是该模式的一个实现,网络的 Socket数据传输是一种特殊的 I/O,Socket 也是一种文件描述符。Socket 也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。常 用 的 Socket 类型有两 种 : 流 式 Socket ( SOCK_STREAM )和数据报式 Socket(SOCK_DGRAM)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket,对应于无连接的 UDP 服务应用。

TCP的C/S架构

image-20221223190503395

示例程序

服务端

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

import (
"fmt"
"log"
"net"
"strings"
)

func dealConn(coon net.Conn) {
defer coon.Close() //此函数结束时,关闭连接套接字

//conn.RemoteAddr().String() 连接客户端的网络地址
ipAdd := coon.RemoteAddr().String()
fmt.Println(ipAdd, "连接成功")

buf := make([]byte, 1024) //缓冲区,用于接收客户端发送的数据

for {
//阻塞等待用户发送的数据
n, err := coon.Read(buf) //n代码接受数据
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>[%d]:%s\n", ipAdd, n, string(result))
if "exit" == string(result) { //如果对方发送”exit",退出此链接
fmt.Println(ipAdd, "退出连接")
return
}
//把接收到的数据转换为大写,再给客户端发送
coon.Write([]byte(strings.ToUpper(string(result))))
}
}

func main() {
//创捷,监听socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
}
defer listenner.Close()

for {
coon, err := listenner.Accept() //阻塞等待客户端连接
if err != nil {
log.Fatal(err)
continue
}
go dealConn(coon)
}
}

客户端

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

import (
"fmt"
"log"
"net"
)

func main() {
coon, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
return
}

defer coon.Close() //关闭

buf := make([]byte, 1024) //缓冲区
for {
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
fmt.Printf("发送的内容:%s\n", string(buf))

//发送数据
coon.Write(buf)

//阻塞等待服务器回复的数据
n, err := coon.Read(buf) //n代码接收的数据长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据[%d]:%s\n", n, string(result))
}
}

HTTP编程

概述

image-20221223193229850

image-20221223193233667

image-20221223193238373

image-20221223193241544

HTTP报文浅析

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

func main() {
//创建、监听 socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生 panic
}
defer listenner.Close()
conn, err := listenner.Accept() //阻塞等待客户端连接
if err != nil {
log.Println(err)
return
}
defer conn.Close() //此函数结束时,关闭连接套接字
//conn.RemoteAddr().String():连接客服端的网络地址
ipAddr := conn.RemoteAddr().String()

fmt.Println(ipAddr, "连接成功")
buf := make([]byte, 4096) //缓冲区,用于接收客户端发送的数据
//阻塞等待用户发送的数据
n, err := conn.Read(buf) //n 代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>:\n%s\n", ipAddr, string(result))
}

image-20221223202444708

image-20221223202527615

image-20221223202542159

image-20221223202552577

响应报文格式

image-20221223203600281

image-20221223203609887

image-20221223203644944

image-20221223203109905

image-20221223203120736

http编程

客户端

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"
"log"
"net/http"
)

func main() {
//get方式请求一个资源
resp, err := http.Get("http://127.0.0.1:8000/")
if err != nil {
log.Fatal(err)
return
}
defer resp.Body.Close() //关闭
fmt.Println("header=", resp.Header)
fmt.Printf("resp status %s\n statuscode %d\n", resp.Status, resp.StatusCode)
fmt.Printf("body type=%T\n", resp.Body)

buf := make([]byte, 2048) //切片缓冲区
var temp string
for {
n, err := resp.Body.Read(buf) //读取body内容
if err != nil {
fmt.Println(err)
return
}
if n == 0 {
fmt.Println("读取内容结束")
break
}
temp += string(buf[:n]) //累加读取的内容
}
fmt.Println("buf=", string(temp))
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"net/http"
)

//服务端编写的业务逻辑处理程序
//header函数:myHandler(w http.ResponseWriter, r *http.Request)具有 签名的函数

func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功") //r.RemoteAddr远程网络地址
fmt.Println("method=", r.Method) //请求方法
fmt.Println("url=", r.URL)
fmt.Println("header=", r.Header)
fmt.Println("body=", r.Body)
w.Write([]byte("hello go")) //给客户端回复数据
}

func main() {
http.HandleFunc("/go", myHandler)

//该方法用于在指定的 TCP 网络地址 addr 进行监听,然后调用服务端处理程序来处理 传入的连接请求。
//该方法有两个参数:第一个参数 addr 即监听地址;第二个参数表示服务端处理程序,通常为空
//第二个参数为空意味着服务端调用 http.DefaultServeMux 进行处理

http.ListenAndServe("127.0.0.1:8000", nil)
}
  • 标题: Go黑马程序员
  • 作者: Olivia的小跟班
  • 创建于 : 2022-12-11 14:50:19
  • 更新于 : 2023-05-27 03:45:16
  • 链接: https://www.youandgentleness.cn/2022/12/11/Go黑马程序员/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
Go黑马程序员