危险

为之则易,不为则难

0%

学习 Go 语言,Updating...

🐹 学习 Go 语言基础知识。

Go 基础认识

关于 Go

🦜 编译型语言(性能高),Go 语言 => 编辑器 => 可执行文件 => 操作系统 + CPU 执行。解释型语言(跨平台好)。

下载 Go

下载 Go,直接下一步安装,我这安装到了 D:\Soft\Go 目录,其中 D:\Soft\Go\bin\go.exe 是编译器,D:\Soft\Go\src 是源码目录。

1
2
3
4
5
6
7
# 查看和 Go 相关的环境变量的情况
go env
# 比较关键的有下面几个
# GO111MODULE=on
# GOROOT=D:\Soft\Go
# GOPATH=C:\Users\dangp\go
# GOPROXY=https://goproxy.cn,direct

如何关闭 GOMOD?

1
go env -w "GO111MODULE=off"
1
2
3
4
5
6
7
8
9
10
11
# 配置环境变量,默认安装后是有的
GOPATH C:\Users\dangp\go

# 下面的三个文件
# src,存放源代码(手动创建)
# pkg,存储编译后生成的包文件,例如第三方包
# bin,可执行文件

# 查看安装的 Go 版本
go version
# go version go1.23.4 windows/amd64

第一个 Go 程序

1
2
3
4
5
6
7
8
package main

// 会去 GOROOT/src 下面查找 fmt
import "fmt"

func main() {
fmt.Println("Hello World")
}
1
2
3
4
5
6
go mod init ifer
go build # 会自动找到 main 包,生成一个可执行的二进制文件,二进制的文件名是 go.mod 中 module 的值
go build -o 包名

# 可以将编译和执行二合一
go run test.go

下载 GoLand

https://www.jetbrains.com.cn/go/download/#section=windows

main.go

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello World")
}

创建项目时,保证 GOROOT 的位置正确。如何运行 Go 文件?

1
2
3
# go build # 会找 main 包进行构建
# go build -o 可执行文件.exe
go run main.go

变量和常量

如何定义变量

变量是对具体值的引用。

1. 先声明再赋值。

1
2
3
var name string
name = "危险"
fmt.Println(name)
1
2
3
// 1 个字节 = 8 个比特位,-128 ~ 127
var x int8 = 127
fmt.Print(x)

2. 声明和赋值一起。

1
2
3
4
var name string = "危险"

// 声明和赋值一起的时候,也可以省略类型
var name = "危险"

3. 变量的默认值是类型的默认值。

1
2
3
var age int
// 默认是类型的默认值
fmt.Println(age) // 0

4. 可以一次性声明多个变量。

1
2
3
4
5
6
7
var (
name string
address string
)
name = "危险"
address = "河南"
fmt.Println(name, address)

或者

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

import "fmt"

func main() {
var (
name string = "IFER"
address string = "河南"
)
fmt.println(name, address)
}

再或者

1
2
var a, b, c = 1, 2, 3
fmt.Println(a, b, c)

5. 短变量(推荐)。

1
2
3
4
// 可以省略 var 和数据类型
name := "危险"
age := 18
fmt.Println(name, age)

如何交换变量

1
2
3
4
5
// 注意点:a 和 b 的类型要保持一致
a := 4
b := 7
a, b = b, a
fmt.Println(a, b) // 7 4

如何打印内存地址?

运算符 描述 实例
& 返回变量存储地址 例如 &a,将给出变量的实际地址
* 指针变量。 例如 *a,表示一个指针变量
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
name := "危险"

// %d 是数字 => decimal
// %s 是字符串
// %p 是指针地址 => pointer
// 注意这儿用的是 Printf,不是 Println
fmt.Printf("name 的值是:%s,内存地址是:%p", name, &name)
}

变量的值拷贝

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

import "fmt"

func main() {
var age1 = 18
var age2 = age1
age2 = 19
fmt.Println(age1, age2) // 18, 19
}

什么是匿名变量

1
2
3
// _ 表示不存储 1,_ 本身是不能使用的
var _, b = 1, 2
fmt.Println(b) // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func test() (int, int) {
return 10, 20
}

func main() {
// Go 语言的设计理念强调显式处理函数的所有返回值,以避免因疏忽而未处理重要的返回结果(如错误信息),从而导致潜在的程序错误
// 匿名变量
a, _ := test()
fmt.Println(a) // 10
}

如何定义常量

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

func main() {
// 定义常量推荐大写
// 1. 常量定义了可以不使用
// 2. 常量不能使用 := 简写
// 3. 常量和变量放置的内存地址不同,常量是放在常量池中的,正常是取不到地址的
// 4. 常量也可以如下方式定义多个
const URL, API string = "wps.cn", "https://www.google.com"
}

iota(了解)

在 Go 语言中,iota 是一个特殊的常量生成器,用于在 const 声明中创建一组相关的常量。它在每一个新的 const 块中从 0 开始,每次遇到新的常量声明时,其值会自动递增 1。

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

import "fmt"

// iota 常量计数器(一组中的),默认是 0,每定义一个常量 iota 的值就会 +1
func main() {
const (
a = iota
b = iota
c = iota
d = 0 // 每定义一个常量 iota 的值就会 +1
e = iota
// 如果在定义 const 的时候,如果下面的常量没有赋值,默认沿用上面一个常量定义的赋值,例如下面的 f,g 就是 iota
f
g
h
// ... 1000+
)
const (
i = iota
j = 8 // 每定义一个常量 iota 的值就会 +1
k = iota // iota + 1 = 2
)
fmt.Println(a, b, c, d, e, f, g, h) // 0 1 2 0 4 5 6 7
fmt.Println(i, j, k) // 0 8 2
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
const (
A = iota
B = 9
C // 如果前面是非 iota,会复用前面的常量值,是 iota 则会 +1
D
)
fmt.Println(A, B, C, D) // 0 9 9 9
}

关于全局和局部变量

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"

// 全局变量,可以不使用
// 1. 定义在 Go 文件的非函数内
// 2. 全局变量必须使用 var 关键字
// 3. 全局变量定义了可以不使用
var c int

func main() {
// 局部变量
a := 1
b := 2
c = a + b
// 如果全局变量有,用的是全局的
fmt.Printf("c的地址是: %p", &c) // 0xbbc570

// 如果局部变量有,用的是局部的
c := a + b
fmt.Printf("c的地址是: %p", &c) // 0xc00000a0f8
c = 8
// 就近原则
fmt.Printf("c的地址是: %p", &c) // 0xc00000a0f8
fmt.Println(c)
}

首字母大写和小写

首字母大写的变量、方法、属性是公有的(能被外部访问),否则是私有的(不能被外部访问)。

基本数据类型

数字默认的类型是 int(int 类型的大小取决于所使用的平台),带 u 的是无符号,只能存正整数,后面的数字表示 2 进制的位数,uint8 还有一个别名 Byte,一个字节=8 个 bit 位。

1
2
3
# 字节(Byte):计算机中数据存储的单位。
# 位(bit):计算机中数据储存的最小单位
1 Byte = 8 bit
1
2
3
4
var x1 byte = 'a'
fmt.Printf("%c %d\n", x1, x1) // a 97
var x2 uint8 = 97
fmt.Printf("%c %d\n", x2, x2) // a 97

整型

1
2
B: Byte 字节
b: 比特位

d7dee1807b1d462095bbb9b7b57a2638~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y2D6YeM5bCB5Yaw_q75

类型 取值范围
int8 -128 到 127
uint8 0 到 255,2 的 8 次方是 256
int16 -32768 到 32767
uint16 0 到 65535
int32 -2147483648 到 2147483647
uint32 0 到 4294967295
int64 -9223372036854775808 到 9223372036854775807
uint64 0 到 18446744073709551615
uint 与平台相关,32 位操作系统上就是 uint32,64 位操作系统上就是 uint64
int 与平台相关,32 位操作系统上就是 int32,64 位操作系统上就是 int64

如何计算

1
2
uint32 => 0 ~ Math.pow(2, 32) - 1
int32 => -Math.pow(2, 31) ~ Math.pow(2, 31) - 1

越界报错

1
2
3
4
5
var x int // 在 32 位操作系统上,int 等价于 int32,四个字节,在 64 位操作系统上,int 等价于 int64,八个字节
x = 9223372036854775808
fmt.Print(x) // overflows int

// cannot use 9223372036854775808 (untyped int constant) as int value in assignment (overflows)

进制转换

1
2
3
4
5
6
7
var a int = 10
fmt.Printf("%d \n", a) // 10 占位符%d表示十进制
fmt.Printf("%b \n", a) // 1010 占位符%b表示二进制
fmt.Printf("%o \n", a) // 12 占位符%o表示八进制
fmt.Printf("%x \n", a) // a 占位符%x表示十六进制

// 0 开始的表示八进制,例如 020,0x 开始的表示十六进制,例如 ox12

查看类型

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

import (
"fmt"
"reflect"
)

func main() {
var x = 1
fmt.Println(reflect.TypeOf(x)) // int
}

特殊的类型 byte 和 rune

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

import "fmt"

func main() {
// uint8 的别名是 Byte(即 1 个字节等于 8 位),十分常用
// 常用来表示 0 ~ 255 之间的整数
var num1 byte = 255
fmt.Printf("%T\n", num1)

// int32 的别名是 rune
var num2 rune = 1000000000
fmt.Printf("%T\n", num2)
}

浮点型

浮点型有两种,float32 和 float64,默认为 float64(可保留的小数点位数更多)

1
2
var f3 = 1.47
fmt.Println(f3, reflect.TypeOf(f3)) // 默认 float64
1
2
3
4
5
// 3 乘以 10 的 -2 次方
// e 的科学计数法的形式来表示都会被认为是浮点型
var x = 3e-2
var y = 4e2
fmt.Println(x, y) // 0.03, 400

布尔

1
2
var b1 = true
fmt.Println(reflect.TypeOf(b1)) // bool

字符串

基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var str = "hello world"

// 1. 索引取值,取一个的时候取的是字符的 ASCII 码值
s1 := str[1]
fmt.Println(s1) // 101,取的是 e 的 ASCII 码值
fmt.Println(string(s1)) // e

// 2. 通过切片的形式取值
s2 := str[2:5]
// s2 := str[2:] // 从第 2 个字符开始到最后一个字符
// s2 := str[:3] // 从第一个字符开始到第 3 个字符
fmt.Println(s2) // llo
fmt.Println(reflect.TypeOf(s2)) // string

// 3. 字符串拼接
s3 := str + " 123"
fmt.Println(s3) // hello world 123

转义符

1
2
var str = "hello world \"🤠\""
fmt.Println(str) // hello world "🤠"

多行字符串

通过 `` 包裹的能严格保留内部的格式。

1
2
3
4
5
var str = `
1. a
2. b
`
fmt.Println(str)

常用方法

方法 介绍
len(str) 求长度
strings.ToUpper,strings.ToLower 生成一个新的全部大写的字符串,生成一个新的全部小写的字符串
strings.ReplaceAll(str, ".", "🦜") 生成一个新的原字符串被指定替换后的字符串
strings.Contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Trim 去除字符串两端匹配的内容
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Split 分割,将字符串按指定的内容分割成数组
strings.Join(a[]string, sep string) join 操作,将数组按指定的内容拼接成字符串

类型转换

Go 语言不存在隐式类型转换,所有的类型转换都必须显式的声明:A = A(B)

1
新类型的值 = 新类型(旧类型的值)
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() {
// 浮点数转整数,会截断,只保留整数部分
a := 5.9 // float
b := int(a) // b 就是 int 类型的 5

fmt.Printf("%T,%.1f\n", a, a) // float64, 5.9
fmt.Printf("%T,%d\n", b, b) // int, 5

// 整数转浮点数
c := 1
d := float64(c)
fmt.Printf("%T,%d\n", c, c) // int, 1
fmt.Printf("%T,%f\n", d, d) // float64, 1.000000

// 布尔类型转换,布尔类型是不支持转换为 int
// var flag bool = true
// f := int(flag)
}
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
// 1. 整形之间转换
// 注意:从大到小转换可能存在类型丢失,例如 int8(200)
var a int8 = 20
// int8 转 int64
fmt.Println(int64(a), reflect.TypeOf(int64(a))) // 20, int64
// int8 转 float64
fmt.Println(float64(a), reflect.TypeOf(float64(a))) // 20, float64

// 2. string 与 int 类型的转换
// int 转 string
x := strconv.Itoa(98)
fmt.Println(x, reflect.TypeOf(x)) // 98, string

// string 转 int
y, _ := strconv.Atoi("98") // A 表示 ASCII 码,意思是将基于 ASCII 码的字符串转换成 int
fmt.Println(y, reflect.TypeOf(y)) // 98, int

// 3. Parse
// 把数字型的字符串转成 int
// 参数1:数字型的字符串
// 参数2:数字型字符串的进制
// 参数3:如果字符串转换的结果超过了 bitSize 的最大值,那么则输出 intBitSize 的最大值
x1, _ := strconv.ParseInt("1000", 10, 8)
fmt.Println(x1) // 127
x2, _ := strconv.ParseInt("1000", 10, 64)
fmt.Println(x2) // 1000

// 把浮点型的字符串转换成 Float
x3, _ := strconv.ParseFloat("1.211", 64)
fmt.Println(x3, reflect.TypeOf(x3)) // 1.211, float64

// 把布尔型的字符串转换成 Boolean
b1, _ := strconv.ParseBool("true")
fmt.Println(b1, reflect.TypeOf(b1)) // true, bool

关于分支

fmt.Scan($num1),接受输入,注意参数要是一个地址。

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"

// 需求:两次输入 123456,提示登录成功
func main() {
var num1 int
var num2 int
// 提示用户输入
fmt.Printf("请输入密码 : \n")
// &num1 地址
fmt.Scan(&num1)
if num1 == 123456 {
fmt.Println("请再次输入密码: ")
fmt.Scan(&num2)
if num2 == 123456 {
fmt.Println("登录成功!")
} else {
fmt.Println("登录失败!")
}
} else {
fmt.Println("登录失败")
}
}

switch 比 if else 更简洁,效率更高!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var choice int
fmt.Println("Choose a number:")
// 这儿是它的地址,&choice,装进去
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Println("牛")
case 2:
fmt.Println("牛牛")
case 3:
fmt.Println("牛牛牛")
case 4:
fmt.Println("牛牛牛牛")
default:
fmt.Println("输入有误")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {

a := false

// 这里的爆红不影响
switch a {
case false:
fmt.Println("1")
fallthrough // 在 case 中一旦使用了 fallthrough,则会强制执行下一个 case 语句
case true:
fmt.Println("2")
case false:
fmt.Println("3")
default:
fmt.Println("6")
}
}

关于循环

求 1 ~ 100 的和。

1
2
3
4
5
6
7
8
9
10
/* var count = 1
for count <= 100 {
fmt.Println(count)
count++
} */
var s = 0
for i := 1; i <= 100; i++ {
s += i
}
fmt.Println(s) // 5050

无限循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// true 可以省略,但是为了可读性,还是建议写上
for true {
var choice int
fmt.Println("Choose a number:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Println("牛")
case 2:
fmt.Println("牛牛")
case 3:
fmt.Println("牛牛牛")
case 4:
fmt.Println("牛牛牛牛")
default:
fmt.Println("输入有误")
}
}

打印一个十行十列的矩形。

1
2
3
4
5
6
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
fmt.Print(" * ")
}
fmt.Print("\n")
}

直接三角形。

1
2
3
4
5
6
7
8
9
*
* *
* * *
* * * *
* * * * *
* * * * * *
* * * * * * *
* * * * * * * *
* * * * * * * * *
1
2
3
4
5
6
7
for i := 0; i < 10; i++ {
for j := 0; j < i; j++ {
fmt.Print(" * ")
}
//fmt.Print("\n")
fmt.Println()
}

输入输出

输出函数

1. fmt.Print(),不换行输出
2. fmt.Println(),换行输出
3. fmt.Printf(),格式化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
name := "John Doe"
age := 18
isMarried := false
salary := 123.4

fmt.Printf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
fmt.Printf("姓名:%v 年龄:%v 婚否:%v 薪资:%v\n", name, age, isMarried, salary)
// %T 表示类型
fmt.Printf("你好的类型是 %T,5 的类型是 %T", "你好", 5)
// %v 任意的占位
fmt.Printf("%v %v", "你好", 5)
// %#v 即便是空字符串也能被看到
fmt.Printf("%#v %#v", "你好", "") // "你好" ""

4. fmt.Sprintf(),不会输出到终端,而是返回加工后的字符串

1
2
3
4
5
// ...
// d => decimal => 十进制
// t => true/false => boolean
info := fmt.Sprintf("姓名:%s 年龄:%d 婚否:%t 薪资:%.2f\n", name, age, isMarried, salary)
fmt.Println(info) // 姓名:John Doe 年龄:18 婚否:false 薪资:123.40

输入函数

1. fmt.Scan()

案例:输入求和。

1
2
3
4
5
6
7
var a, b int
fmt.Print("请输入第一个数字:")
// 为了能让内部操作,所以传地址
fmt.Scan(&a)
fmt.Print("请输入第一个数字:")
fmt.Scan(&b)
fmt.Println("合计:", a+b)
1
2
// 以空格作为分隔符
fmt.Scan(&a, &b)

2. fmt.Scanf(),输入多个时,可以以指定的形式隔开

1
2
3
4
var a, b int
fmt.Scan(&a, &b) // 输入:1 2
//fmt.Scanf("%d + %d", &a, &b) // 输出:1 + 2
fmt.Println(a + b)

3. fmt.Scanln()

了解 Scan 和 Scanln 的区别,如果是 Scan,先输入 1,再敲回车,会继续等待,可以再输入 2,而 Scanln 是遇到回车就结束啦。

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

import "fmt"

func main() {
var a, b int
fmt.Print("请输入两个整数,用空格分隔,然后按回车键: ")
fmt.Scanln(&a, &b)
fmt.Printf("你输入的两个整数是: %d 和 %d\n", a, b)
}

Go 核心语法

指针

概念:所有数据必须放到内存中,将内存中数据的编号称为指针,指针也可以理解为地址。

符号 名称 作用
& 变量 取址符 返回变量所在的地址
* 指针变量 取值符 返回指针指地址存储的值

基本操作

如何获取数据的指针/地址?

1
2
3
var x = 1
// 取址
fmt.Println(&x)

如何使用指针?

1. 通过 *类型 定义指针变量

2. 通过 &变量 取出地址(取址),为指针变量赋值。

3. 通过 * 访问指针变量中地址所指向的值,叫取值。

1
2
3
4
5
6
7
8
9
10
11
var x = 1
// 1. 定义指针变量 p
var p *int
// 2. 为指针变量赋值,赋的是一个地址值(p 存储的值是地址,同时 p 也有自己的地址)
p = &x
fmt.Println(p) // p 是一个指针变量,它存储了变量 x 的内存地址。当你打印 p 时,输出的就是 x 的内存地址
// 3. 访问指针变量中地址值所指向的值,叫取值
fmt.Println(*p)
fmt.Println(&p) // &p 表示取指针变量 p 本身的内存地址。这里是对指针变量 p 进行取地址操作,即获取 p 的地址
// 4. 通过 *指针变量,可以操作地址值对应的值
*p = 888

xxx.drawio (2)

上面演示的是 int 类型的指针变量,其实也可以有其他类型的指针变量,例如 string 类型的指针变量。

1
2
var s string = 'ifer'
var p *string = &s

双指针,画图理解下面的操作。

1
2
3
4
5
6
var a = "张飞"
var b = &a
// b 的地址给了 c
var c = &b
**c = "吕布"
fmt.Println(a) // 吕布

xxx

指针赋值

Go 语言所有的都是值拷贝,只不过这个值是普通值还是地址值。

拷贝的是普通值!

1
2
3
4
var a = "张飞"
var b = a
b = "吕布"
fmt.Println(a, b) // 张飞 吕布

拷贝的是地址值!

1
2
3
4
var a = "张飞"
var b = &a // 完整写法 => var b *int = &a,此时拷贝的是一个地址值
*b = "吕布"
fmt.Println(a) // 吕布

下面输出结果是什么?

1
2
3
4
5
6
7
var x = "张飞"
var y = &x // x 和 y 相互影响,y 是 *int 类型的指针变量
var z = *y // z => 张飞
z = "吕布" // z => 吕布
fmt.Println(x) // 张飞
fmt.Println(*y) // 张飞
fmt.Println(z) // 吕布

数组【指针】

数组指针,首先应该是一个指针,指向了一个数组。

记住:* 是取值!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 创建数组
arr := [4]int{1, 2, 3, 4}

// 2. 创建一个数组指针
var p *[4]int

// 3. 数组指针指向这个数组的地址
p = &arr

// 4. 通过数组指针来修改数组
(*p)[0] = 4

// 5. 也可以使用简写(数组指针所特有的,例如 int 指针就不行)
p[1] = 7

fmt.Println(*p) // [4 7 3 4]

指针【数组】

指针数组,首先应该是一个数组,里面保存的是指针(有指针组成的数组)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s1 := "张飞"
s2 := "吕布"
s3 := "赵云"

// 1. 创建一个指针数组给了变量 arr
arr := [3]*string{&s1, &s2, &s3}
fmt.Println(arr) // [0xc0000220a0 0xc0000220b0 0xc0000220c0]

// 2. 通过指针修改 s1 的值,arr[0] 是一个地址,那么 *arr[0] 就是值
*arr[0] = "潘凤"
fmt.Println(s1) // 潘凤

// 3. 修改 s1 的值,后面通过指针获取访问也会同步发生变化
s1 = "黄盖"
fmt.Println(*arr[0]) // 黄盖

指针函数

指针函数一般说的是:函数的返回值是指针!

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

import "fmt"

// 1. 创建一个函数,返回一个数组指针
func foo() *[4]string {
arr := [4]string{"张飞", "吕布", "赵云"}
return &arr
}

func main() {
// 2. 调用了这个函数后,可以得到一个数组指针
p := foo()
// 3. 使用
fmt.Println((*p)[0]) // 张飞
(*p)[0] = "潘凤" // 或 p[0] = "潘凤"
fmt.Println(p[0]) // 潘峰
}

指针作为函数参数

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

import "fmt"

func foo(p *int) {
// 2. 修改地址对应的值
*p = 2
}

func main() {
a := 1
// 1. 传递的是地址
foo(&a)
// 3. 外部也会得到改变
fmt.Println("a:", a) // a: 2
}

指针的题目

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 10
// 这儿发生了值拷贝,x 和 y 互不影响
var y = x
// 地址拷贝,x 和 z 相互影响
// z 是一个指针变量
var z = &x
x = 20
fmt.Println(y) // 10
fmt.Println(*z) // 20

// 可以通过 *指针变量,来修改指针变量对应的值
*z = 30
fmt.Println(x) // 30
1
2
3
4
5
6
7
8
9
10
11
var x = 10
// 地址拷贝,x 和 y 相互影响
// y 是一个指针变量
var y = &x
// z => 10,z 是一个普通值
// *y 是取值,把普通值给了 z
var z = *y
x = 20
fmt.Println(x) // 20
fmt.Println(*y) // 20
fmt.Println(z) // 10
1
2
3
4
5
6
7
var a = 100
// b 是一个指针变量,a 和 b 相互影响
var b = &a
// c 是一个指针类型的指针变量,c 和 b 相互影响,把 b 的地址(注意是 b 的地址,而不是 b 的内容)给了 c
var c = &b
**c = 200
fmt.Println(a) // 200

new 函数

🤔 mark

new 的返回值是一个指针,用来分配内存,make 用来初始化 slice、map 和 channel。

基本数据类型声明后都有默认零值,例如 int、string、bool 等,例如:

1
2
var x int
fmt.Println(x) // 0

指针类型是引用类型,如果声明后并没有默认值,直接操作是会报错的,如下:

1
2
3
4
// p 是一个 int 类型的指针类型
var p *int
fmt.Println(p) // <nil>,空对象,不能直接操作,开辟了一个空间里面存了一个 nil,其实如果这个空间里面是一个地址,那就可以操作了
*p = 100 // invalid memory address or nil pointer dereference

如何处理呢?答案:new,它的返回值是指向 new 的参数类型的指针。

1
2
3
4
5
// var p *int
// p = new(int) // 弄一个空间,空间里面的地址是 0x11,把这个地址又指向了另一个空间,这个空间里面存了 int 的默认值 0
var p *int = new(int)
*p = 100
fmt.Println(p, *p) // 0xc000094098, 100

数组

特点:数组是一组相同类型的数据的有序集合;定义的时候需要指定长度;数组一旦定义了长度不可改变。

声明数组

语法:

1
var 数组名 [元素数量]元素类型

举例:

1
2
3
// 长度为 5 的 string 数组
var names [5]string // ["", "", "", "", ""]
fmt.Println(names, reflect.TypeOf(names)) // [ ] [5]string

注意下面的 x 和 y 的数据类型是不同的。

1
2
var x [3]string
var y [5]string

初始化数组

先声明再赋值

1
2
3
4
var names [5]string
names[0] = "张飞"
names[1] = "吕布"
fmt.Println(names) // [张飞 吕布 ]

声明并赋值

1
2
var names = [5]string{"张飞", "吕布"}
fmt.Println(names) // [张飞 吕布 ]

使用 := 快速初始化

1
2
arr := [5]string{"吕布", "张飞"}
fmt.Println(arr2) // [张飞 吕布 ]

不限长度的数组

Go 的编译器会自动根据数组的长度来给 ... 赋值,自动推导长度。

1
2
var names = [...]string{"张飞", "吕布"}
fmt.Println(names) // [张飞 吕布 ]

通过索引设置数组

如何给数组的某几个特定的 index 位置赋值,其他没有操作到的索引对应到值是此类型的默认值。

1
2
var names = [...]string{0: "张飞", 5: "吕布"}
fmt.Println(names) // [张飞 吕布]
1
2
3
var names [10]string
names = [10]string{3: "张飞", 7: "吕布"}
fmt.Println(names) // [ 张飞 吕布 ]

访问和修改数组元素

基于索引访问和修改数组

1
2
3
4
5
6
var names = [...]string{"张飞", "吕布", "赵云", "典韦"}
// 访问
fmt.Println(names[0]) // 张飞
// 修改
names[0] = "黄盖"
fmt.Println(names[0]) // 黄盖

切片取值

🤔 mark

字符串切出来的还是字符串,数组切出来的结果是切片。

1
2
3
4
var names = [...]string{"张飞", "吕布", "赵云", "典韦"}
fmt.Println(names[0:2]) // [张飞 吕布],注意这儿取出来的是切片
fmt.Println(names[0:]) // [张飞 吕布 赵云 典韦]
fmt.Println(names[:2]) // [张飞 吕布]

遍历数组

1
2
3
4
5
6
7
8
9
10
var names = [...]string{"张飞", "吕布", "赵云", "典韦"}
// 1. 遍历数组
for i := 0; i < len(names); i++ {
fmt.Println(names[i])
}
// 2. for range
// Goland 快捷方式 数组.for,推荐
for _, value := range names {
fmt.Println(value)
}

for range 循环(数组、切片、映射)时候的 value 是值的副本,对这个副本的修改不会影响原数据。

1
2
3
4
5
6
7
var names = [...]string{"张飞", "吕布", "赵云", "典韦"}
for _, value := range names {
value = "潘凤"
// value = "潘凤" 只是修改了这个副本的值,并不会影响到原数组 names 中的元素
fmt.Println(value)
}
fmt.Println(names) // [张飞 吕布 赵云 典韦]

数组是值类型特点

🤔 mark

体现:赋值和函数传参。

1
2
3
4
5
6
arr1 := [4]int{5, 2, 4, 7}
// 数组的值传递和 int 等基本类型一致
arr2 := arr1
arr2[0] = 8
// 修改 arr2 后发现 arr1 并没有变化
fmt.Println(arr1) // [5 2 4 7]

数组的冒泡排序

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

import "fmt"

// 冒泡:两两比较,大的往后移或者前移,每次可以筛选出一个最大或者最小的数

func main() {
arr := [...]int{1, 3, 4, 2, 0}
for i := 0; i < len(arr)-1; i++ {
// 筛选出来最大数字以后,我们下次不需要将它再计算了
for j := 0; j < len(arr)-i-1; j++ {
if arr[j] < arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
fmt.Println(arr)
}

二维数组的定义和遍历

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

import "fmt"

func main() {
// 1. 如何定义二维数组?
// 长度是 3,里面数组的长度是 4 的 int 型
arr := [3][4]int{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11},
}
// 2. 如何遍历二维数组
// 2.1. 方式1
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Println(arr[i][j])
}
}
// 2.2 for range
for _, v := range arr {
for i := range v {
fmt.Println(v[i])
}
}
}

切片

🤔 mark

切片是对现有数组的引用,他是一个动态的长度,所以,定义切片的时候不需要指定长度。切片本身不存储任何数据,都是底层的数组来存储的,所以修改了切片也就是修改了这个数组中的数据。

基本概念

从概念上面来说 slice 像一个结构体,这个结构体包含了三个元素:

1. 指针:「指向数组」 的开始位置。

2. 长度:即 slice 的长度。

3. 容量:也就是 slice 开始位置到数组的最后位置的长度。

声明切片的两种方式

声明并初始化切片

1
2
3
4
5
6
7
8
9
// 1. 定长的是数组
arr := [4]string{"张飞", "吕布", "赵云"}
// 2. 定义切片的时候可以指定初始数据
s := []string{"张飞", "吕布", "赵云"}
s[0] = "潘凤"
// 观察数据和切片的类型有什么不同?
fmt.Printf("%T, %T\n", arr, s) // [4]string, []string
// 3. 获取切片中的某个数据
fmt.Println(s[0]) // 潘凤

通过 make 函数

只声明的切片,后续不能直接赋值,需要通过 make 进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 定长的是数组
arr := [4]string{"张飞", "吕布", "赵云"}
fmt.Println(arr) // [张飞 吕布 赵云 ]

// 2. 不定长的是切片
var s1 []string
fmt.Println(s1) // []

// 3. 如何判断切片为空?
// 初始切片默认是 nil
if s1 == nil {
fmt.Println("切片是空的")
}

// 4. 只是声明的切片,不能直接赋值,可以通过后面的 make 进行初始化
s1[0] = "潘凤" // panic: runtime error: index out of range [0] with length 0

如何通过 make 初始化切片?

语法:make([]type, length, capacity)

1
2
3
4
5
6
7
8
9
var s []string
s = make([]string, 5, 10)

// 当然也可以简写
// s := make([]string, 5, 10)

s[0] = "潘凤"

fmt.Println(s) // [潘凤 ]

🤔 mark

一个注意点:当切片 s 的长度为 0 时,不能直接通过索引 s[0] 来赋值,但是可以通过 append 来追加数据。

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

import "fmt"

func main() {
s := make([]string, 0, 10)
// s[0] = "张飞"
s = append(s, "张飞")
fmt.Println(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
// 1. 先初始化一个定长的是数组
var arr = [3]string{"张飞", "吕布", "赵云"}

// 2. 基于数组切出来的是切片
var s1 = arr[1:3]
fmt.Println(s1, reflect.TypeOf(s1)) // [吕布 赵云] []string

// 3. 再切
var s2 = arr[:]
fmt.Println(s2, reflect.TypeOf(s2)) // [张飞 吕布 赵云] []string

// 4. 基于切片再切出切片
var s3 = s2[0:2]
s3[0] = "潘凤"

fmt.Println(s1) // [吕布 赵云]
fmt.Println(s2) // [潘凤 吕布 赵云]
fmt.Println(s3) // [潘凤 吕布]
fmt.Println(arr) // [潘凤 吕布 赵云]

// 5. 观察地址,查看切片的内存地址
fmt.Printf("原数组:%p\n", &arr) // 原数组 0xc000092150
fmt.Printf("%p\n", s1) // 0xc000092160
fmt.Printf("%p\n", s2) // 0xc000092150
fmt.Printf("%p\n", s3) // 0xc000092150

切片原理

🤔 mark

1
2
3
4
5
6
7
8
9
10
11
12
var names = [5]string{"张飞", "吕布", "赵云", "许褚", "潘凤"}
fmt.Println(&names[0], &names[1], &names[2], &names[3], &names[4])
s1 := names[0:3]
s2 := names[2:5]
s3 := names[0:2]
fmt.Println(&s1[0], &s2[0], &s3[0])

// 测试下面的地址?其实就是数组前三项的地址
fmt.Println(&s1[0], &s1[1], &s1[2])

// 如何打印切片的地址?
fmt.Printf("%p, %p, %p", &s1, &s2, &s3)
xxx16

切片和原数组是关联的吗?什么时候会失去关联

🤔 mark

在 Go 中,切片(slice)和原数组是关联的,这种关联的本质是:切片通过内部的指针指向原数组的一部分数据,因此切片和原数组共享同一块底层内存。

当切片发生扩容时,会创建一个新的底层数组,此时切片与原数组的关联会被打破(切片的指针会指向新数组)。扩容的触发条件是:当切片的长度(len)即将超过其容量(cap)时,对切片执行 append 操作会触发扩容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = [3]string{"张飞", "吕布", "赵云"}
var s1 = arr[1:3] // s1 长度 len=2,容量 cap=2(从 arr[1] 到数组末尾共 2 个元素)

// 此时 s1 与 arr 关联,修改 s1 会影响 arr
s1[0] = "潘凤"
fmt.Println(arr) // 输出 [张飞 潘凤 赵云]

// 对 s1 执行 append,长度将超过容量(2 → 3),触发扩容
s1 = append(s1, "关羽") // 此时 s1 会创建新的底层数组,与原 arr 失去关联

// 再次修改 s1,原数组不会受影响
s1[0] = "颜良"
fmt.Println(arr) // 仍然输出 [张飞 潘凤 赵云](原数组未变)
fmt.Println(s1) // 输出 [颜良 赵云 关羽](新数组中的值)

遍历切片的两种方式

1
2
3
4
5
6
7
8
9
10
11
s := []string{"张飞", "吕布", "赵云"}

// 方式 1
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}

// 方式 2
for i, v := range s {
fmt.Println(s[i], v)
}

通过 append 追加数据

基本使用

1
2
3
4
5
// 无初始内容,注意如果这样:s1 := make([]string, 2, 3),则表示初始内容为 2 个零值,再次 append 会追加到这两个零值之后
// 所以使用 append 操作的话,make 的时候,长度一般指定为 0
s1 := make([]string, 0, 3)
s1 = append(s1, "张飞", "吕布")
fmt.Println(s1, len(s1), cap(s1)) // [张飞 吕布] 2 3
1
2
3
4
5
6
7
8
xxx := make([]string, 3, 5)
xxx[0] = "a"
xxx[1] = "b"
xxx[2] = "c"

// 容量够,会影响底层数组,但不会影响 xxx
zzz := append(xxx, "d")
fmt.Println(zzz)

添加会自动扩容

容量只有 3 个,那能放超过 3 个的吗?可以,通过 append 操作切片,当容量不够时,会翻倍扩容。

1
2
3
s1 := make([]string, 0, 3)
s1 = append(s1, "张飞", "吕布", "赵云", "典韦")
fmt.Println(s1, len(s1), cap(s1)) // [张飞 吕布 赵云 典韦] 4 6

合并两个切片

1
2
3
4
5
s1 := []string{"张飞", "吕布"}
s2 := []string{"赵云", "典韦"}
s1 = append(s1, s2...)

fmt.Println(s1) // [张飞 吕布 赵云 典韦]

扩容分析(重)

🤔 mark

向切片中添加数据的时候,如果没有超过容量,直接添加,如果超过了这个容量,就会自动扩容,cap 成倍的增加,拷贝,切片一旦扩容(超出了原来的容量),就是重新指向一个新的底层数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s1 := []string{"张飞", "吕布", "赵云"}
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:3,cap:3
// 底层数组首地址
fmt.Printf("%p\n", s1) // 0xc00001c180

s1 = append(s1, "典韦", "潘凤")
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:5,cap:6
// 扩容后,可以发现 s1 重新指向了新地址
fmt.Printf("%p\n", s1) // 0xc00007c0c0

s1 = append(s1, "黄忠", "张辽")
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:7,cap:12
// 扩容后,s1 重新指向了新地址
fmt.Printf("%p\n", s1) // 0xc0000000c0

s1 = append(s1, "马超", "许褚")
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:9,cap:12
// 没有超出原来的容量,地址不变
fmt.Printf("%p\n", s1) // 0xc0000000c0

修改扩容后的数据会影响原数组吗

1
2
3
4
5
6
7
8
9
10
11
12
s1 := []string{"张飞", "吕布", "赵云"}
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) // len:3,cap:3
fmt.Printf("%p\n", s1) // 0xc00001c180

s2 := append(s1, "典韦", "潘凤")
fmt.Printf("len:%d,cap:%d\n", len(s2), cap(s2)) // len:5,cap:6
// 扩容后,可以发现 s2 重新指向了新地址
fmt.Printf("%p\n", s2) // 0xc00007c0c0

// 对 s2 的修改,肯定不会影响原来的 s1
s2[0] = "许褚"
fmt.Println(s1[0]) // 张飞

一个注意点

🤔 mark

1
2
fmt.Printf("%p\n", s2)         // 底层数组的地址
fmt.Printf("%p\n", &s2) // slice 变量本身的地址,只要是同一个变量就不会变化
1
2
3
4
5
6
7
8
9
10
11
12
slice1 := make([]int, 5, 10)
slice2 := append(slice1, 1)
fmt.Println(slice2) // [0 0 0 0 0 1]

// 证明原底层数组变了
slice3 := slice1[:6]
fmt.Println(slice3) // [0 0 0 0 0 1],

slice4 := append(slice1, 3) // 注意是从 slice1 开始添加,会覆盖
fmt.Println(slice4) // [0 0 0 0 0 3],说明原底层数组变了

fmt.Println(slice1) // [0 0 0 0 0],slice1 是一个切片,不存储任何实际值,这个切片的长度是 5,取不到后面的 3

测试题目

1
2
3
4
5
6
7
8
9
10
l := make([]int, 5, 10) // l => {0, 0, 0, 0, 0}
v1 := append(l, 1) // 得到了一个新切片 v1
fmt.Println(v1) // v1 => {0, 0, 0, 0, 0, 1}
fmt.Printf("%p\n", &v1)
v2 := append(l, 2) // 格外注意,从 l 开始扩展的!
fmt.Println(v2) // v2 => {0, 0, 0, 0, 0, 2}
fmt.Printf("%p\n", &v2)
fmt.Println(v1) // v2 => {0, 0, 0, 0, 0, 2}

// v1 和 v2 共享的是同一个底层数组

开头添加

1
2
3
4
var s1 = []string{"张飞", "吕布"}
// 第一个参数要求是一个切片
s2 := append([]string{"典韦"}, s1...)
fmt.Println(s2) // [典韦 张飞 吕布]

任意位置插入

1
2
3
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{x, y, z}, a[i:]...)...) // 在第i个位置插入切片

举例如下:

1
2
3
4
5
6
var s1 = []string{"张飞", "吕布"}
// 如何在张飞后面添加一个黄忠?
/* temp := append([]string{"黄忠"}, s1[1:]...)
s2 := append(s1[:1], temp...) */
s2 := append(s1[:1], append([]string{"黄忠"}, s1[1:]...)...)
fmt.Println(s2) // [张飞 黄忠 吕布]

如何删除

方法 1

删除切片 a 中索引为 i 的元素,公式:

1
a = append(a[:index], a[index+1:]...)
1
2
3
4
var s1 = []string{"张飞", "吕布", "赵云", "许褚"}
// 删掉赵云
s1 = append(s1[:2], s1[3:]...)
fmt.Println(s1) // [张飞 吕布 许褚]

方法 2

1
2
3
4
5
6
7
8
9
10
var s1 = []string{"张飞", "吕布", "赵云", "许褚"}
// 如何按值删除?循环遍历,把不等于 target 的添加进新切片
var s2 []string
var target = "赵云"
for _, v := range s1 {
if v != target {
s2 = append(s2, v)
}
}
fmt.Println(s2) // [张飞 吕布 许褚]

方法 3

1
2
3
4
5
6
7
8
9
10
var s1 = []string{"张飞", "吕布", "赵云", "许褚"}
// 如何按值删除?
var target = "赵云"
for i, v := range s1 {
if v == target {
// 对 s1 进行了重新赋值
s1 = append(s1[:i], s1[i+1:]...)
}
}
fmt.Println(s1) // [张飞 吕布 许褚]

学生管理版本 1(切片里面装字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var students []string
for {
fmt.Println(`
1. 查看学生
2. 添加学生
3. 删除学生
4. 修改学生
5. 退出学生
`)
var choice int
fmt.Println("Enter your choice: ")
fmt.Scan(&choice)
switch choice {
case 1:
fmt.Println("===查看学生===")
for i, v := range students {
fmt.Println(i+1, ":", v)
}
case 2:
fmt.Println("===添加学生===")
var addStudent string
fmt.Println("请输入要添加的学生姓名:")
fmt.Scan(&addStudent)
students = append(students, addStudent)
fmt.Println("添加成功!")
case 3:
fmt.Println("===删除学生===")
var delStudent string
fmt.Println("请输入要删除的学生姓名:")
fmt.Scan(&delStudent)
for i, v := range students {
if v == delStudent {
students = append(students[:i], students[i+1:]...)
}
}
fmt.Println("删除成功!")
case 4:
fmt.Println("===修改学生===")
fmt.Println("请输入需要修改的名字: ")
var name string
fmt.Scan(&name)
fmt.Println("请输入新名字: ")
var newName string
fmt.Scan(&newName)
for i, v := range students {
if v == name {
students[i] = newName
fmt.Println("修改成功")
}
}
fmt.Println("修改失败")
case 5:
fmt.Println("===退出学生===")
// 硬退出
os.Exit(200)
default:
fmt.Println("===非法输入===")
}
}

切片是引用类型的特点

1
2
3
4
5
6
7
8
9
10
11
12
13
s1 := []string{"张飞", "吕布", "赵云", "许褚"}
// 一切皆为值拷贝,只不过拷贝的值里面是地址
s2 := s1
s1[0] = "潘凤"
// 虽然修改的是 s1,发现 s2 也发生了变化,这一点和数组不同
fmt.Println(s2) // [潘凤 吕布 赵云 许褚]

// 底层数组的地址
fmt.Printf("%p, %p\n", s1, s2) // 0xc0000a80c0 0xc0000a80c0
// 即底层数组第一个元素的地址
fmt.Println(&s1[0], &s2[0]) // 0xc0000a80c0 0xc0000a80c0
// 切片本身的地址
fmt.Printf("%p, %p\n", &s1, &s2) // 0xc000010018 0xc000010030

切片排序

整型

1
2
3
4
5
6
a := []int{5, 1, 7, 9}
// 没有返回值
// sort.Sort(sort.IntSlice(a)) 可以简写为 sort.Ints(a)
sort.Ints(a)
// a 变化了
fmt.Println(a) // [1 5 7 9]

字符串

1
2
3
b := []string{"melon", "banana", "strawberry", "apple"}
sort.Strings(b)
fmt.Println(b) // [apple banana melon strawberry]

浮点数

1
2
3
c := []float64{2.2, 1.5, 3.12, 9, 8.5}
sort.Float64s(c)
fmt.Println(c) // [1.5 2.2 3.12 8.5 9]

通过 sort.IntSlice 将普通切片转换为可排序的类型,sort.Reverse 反转排序逻辑,sort.Sort 执行排序操作,最终实现了对整数切片的降序排序。

1
2
3
4
5
6
7
a := []int{5, 1, 7, 9}
// 也就是说,IntSlice 其实就是 []int 的一种命名类型
// 但不同的是,IntSlice 实现了 sort.Interface(包含 Len(), Less(i, j int), Swap(i, j int)),所以可以用在 sort.Sort 里

// 调用 sort.Reverse(sort.IntSlice(a)) 只是得到了一个“带反转规则的排序接口”,只构造规则,不会执行排序,要真正“反转排序”,必须搭配 sort.Sort 一起用
sort.Sort(sort.Reverse(sort.IntSlice(a)))
fmt.Println(a)

如果是一个数组,需要先转成切片再排序。

深拷贝和浅拷贝

值类型的数据,默认都是深拷贝,例如 array、int、float、string、bool、struct 等。

引用类型的数据,默认都是浅拷贝,例如 slice、map、chan,拷贝的是地址,会导致多个变量指向同一块内存。

如何实现切片的深拷贝?

for 循环

1
2
3
4
5
6
7
s1 := []string{"张飞", "吕布", "赵云", "许褚"}
s2 := make([]string, 0)
for i := 0; i < len(s1); i++ {
s2 = append(s2, s1[i])
}
s2[0] = "潘凤"
fmt.Println(s1[0]) // 张飞

append

1
2
3
4
5
s1 := []string{"张飞", "吕布", "赵云", "许褚"}
s2 := make([]string, 0)
s2 = append(s2, s1...)
s2[0] = "潘凤"
fmt.Println(s1[0]) // 张飞

copy

1
2
3
4
5
6
7
8
9
10
11
var s1 = []string{"张飞", "吕布", "赵云", "许褚"}
var s2 = make([]string, len(s1))
// 把 s1 拷贝给 s2
copy(s2, s1)
fmt.Println(s2) // [张飞 吕布 赵云 许褚]

s3 := []int{4, 5}
s4 := []int{6, 7, 8, 9}
// 把 s3 拷贝给 s4
copy(s4, s3)
fmt.Println(s4) // [4 5 8 9]

make 函数

针对的是指针、切片、映射、管道。

1
2
3
4
5
6
7
8
9
// 没有底层数组,不知道开多大的,不能赋值
/* var x []int
x[0] = 1
fmt.Println(x) */

// 开了底层数组,如果不写容量,默认和长度一样
var x = make([]int, 3)
x[0] = 1
fmt.Println(x) // [1 0 0]

课后练习

1. 描述执行过程

1
2
3
4
var s []string
// 报错,解决:make
s[0] = "张飞"
fmt.Println(s) // [张飞]
1
2
3
4
5
6
7
// 不写容量,默认和长度一致
s := make([]string, 3)
// 注意,下面的写法并不会开辟空间,也就意味着不能 s[0] ...
// var s []string
// 需要扩容,拷贝原来数组,再追加新的数据,s 指向了新的数组
s = append(s, "张飞", "吕布", "赵云")
fmt.Println(s) // [ 张飞 吕布 赵云]

🤔 mark

关键点:s 会复制原数组重新扩容,s 本身的地址没有改变(内容变了)。

xxx (15)

2. 描述执行过程

1
2
3
4
5
6
7
p1 := 1
// p2 是 int 类型的指针变量
p2 := &p1
// 先走 *p2,然后是 ++
*p2++
fmt.Println(p1) // 2
fmt.Println(*p2) // 2

3. 输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a := [3]int{0, 1, 2}
s := a[1:2] // {1}

// 没扩容之前都会影响原数组
s[0] = 11 // {11}
// 会把原来的 2 给覆盖掉
s = append(s, 12) // {11, 12}
// 扩容了,s 指向了新的数组
s = append(s, 13) // {11, 12, 13}
fmt.Println(s) // {11, 12, 13}
// 不会影响原数组,因为 s 指向了新的数组
s[0] = 21
fmt.Println(a) // {0, 11, 12}
fmt.Println(s) // {21, 12, 13}

xxx (12)

如何理解 s[0] = 11,a 数组对应位置变成了 11,s 也变成了 {11}

xxx (11)

如何理解 s = append(s, 12)

xxx (10)

如何理解 s = append(s, 13),注意扩容 2 倍指的是原 s 容量的 2 倍。

扩容之后,Go 语言会重新分配一个新的底层数组,把原来的数据复制过去,然后在新数组里添加新元素。“原来的数据” 就是在扩容操作之前切片 s 中已经存在的元素,即 [11, 12]

xxx (9)

如何理解 s[0] = 21

1
2
3
4
5
6
7
8
9
10
11
12
a := [3]int{0, 1, 2}
s := a[1:2]

s[0] = 11
s = append(s, 12)
s = append(s, 13)
fmt.Println(s) // {11, 12, 13}
s[0] = 21
fmt.Println(a) // {0, 11, 12}
fmt.Println(s) // {21, 12, 13}
fullSlice := s[:cap(s)]
fmt.Println(fullSlice) // [21 12 13 0]

4. 切片反转

1
2
3
4
5
6
7
s := []int{1, 2, 3, 4, 5}
// 反转
// 注意这儿是一句:i, j := 0, len(s)-1;
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
fmt.Println(s)
1
2
i, j := 1, 2
fmt.Print(i, j) // 1 2

5. 观察结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//var a = [5]int{1, 2, 3, 4, 5}
//for i, v := range a {
// // 每次循环 v 的地址一直是同一个地址
// //fmt.Println(i, v)
// // 每次循环 ret 的地址肯定不一样
// var ret int
// ret = v
// fmt.Println(&ret)
//}

var a = [5]int{1, 2, 3, 4, 5}
var r [5]int
// 直接 range a 会拷贝数组,&a 不会拷贝数组,是原数组
for i, v := range &a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)

5. new 和 make 的区别

make 初始化引用开辟空间

new 初始化指针开辟空间

6. 观察结果

1
2
3
4
5
6
7
8
9
arr := [4]int{10, 20, 30, 40}
s1 := arr[0:2]
s2 := s1
s3 := append(append(append(s1, 1), 2), 3)
s1[0] = 1000
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(arr)

学生管理版本 2(切片里面装切片)

二维切片。

1
2
3
4
5
6
// var s = []string{"张飞"}
var s = [][]string{
[]string{"张飞", "18", "深圳"},
[]string{"赵云", "19", "河南"},
}
fmt.Println(s[1][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
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
package main

import "fmt"

func main() {
var stus [][]string

fmt.Println(`
1. 查看学生
2. 添加学生
3. 删除学生
4. 修改学生
5. 退出系统
`)

for {
fmt.Print("请输入你的选择:")
var choice int
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Println("=====查看学生=====")
for i, v := range stus {
fmt.Printf("%d. 姓名:%s 年龄:%s 性别:%s\n", i+1, v[0], v[1], v[2])
}
case 2:
fmt.Println("=====添加学生=====")
fmt.Print("请输入学生姓名:")
var name string
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
var age string
fmt.Scanln(&age)
fmt.Print("请输入学生性别:")
var gender string
fmt.Scanln(&gender)
stus = append(stus, []string{name, age, gender})
fmt.Println("添加成功")
case 3:
fmt.Println("=====删除学生=====")
fmt.Print("请输入学生姓名:")
var name string
fmt.Scanln(&name)
for i, v := range stus {
if v[0] == name {
stus = append(stus[:i], stus[i+1:]...)
fmt.Println("删除成功")
break
}
}
case 4:
fmt.Println("=====修改学生=====")
fmt.Print("请输入要修改的学生姓名:")
var name string
fmt.Scanln(&name)
for i, v := range stus {
if v[0] == name {
fmt.Print("请输入学生新名:")
var name string
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
var age string
fmt.Scanln(&age)
fmt.Print("请输入学生性别:")
var gender string
fmt.Scanln(&gender)
stus[i] = []string{name, age, gender}
fmt.Println("修改成功")
break
}
}
case 5:
fmt.Println("=====退出系统=====")
// os.Exit(200)
return
default:
fmt.Println("输入错误,请重新输入")
}
}
}

切片里面放多类型

1
2
3
// interface{} 是空接口,可以放任意类型
stu := []interface{}{"吕布", 18, "河南"}
fmt.Println(stu) // [吕布 18 河南]

Map 映射

基本概念

Map 是一种无序的键值对的集合,所以我们可以像迭代数组和切片那样迭代它,Map 也是引用类型,语法如下:

1
var map1 map[key]value

声明的两种方式

方式一:make

1
2
3
4
5
6
7
8
9
10
// 1. 声明切片
var map1 map[string]string
// 只是声明了但是没有初始化,默认是 nil,此时不能 map1[x]=y 赋值
if map1 == nil {
fmt.Println("map 没有初始化默认是 nil")
}
// 2. 需要进一步使用 make 方法创建
map1 = make(map[string]string)
map1["address"] = "IFER"
fmt.Println(map1) // map[address:IFER]

方式二:声明和初始化可以一起

1
2
3
var map2 = map[string]int{"Go": 100, "Java": 10, "C": 60}
fmt.Println(map2)
fmt.Printf("%T\n", map2) // map[string][int]

操作 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
// 1. 声明 map
var map1 map[int]string

// 2. 进一步通过 make 初始化
map1 = make(map[int]string)
map1[100] = "JavaScript"
map1[200] = "Java"

fmt.Println(map1) // map[100:JavaScript 200:Java]
fmt.Println(map1[200]) // Java

// 3. 取值的时候可以判断
// 不存在,会输出 string 类型的零值
value, ok := map1[1]
if ok {
fmt.Println("map key 存在,value 是:", value)
} else {
fmt.Println("map key 不存在!")
}

// 4. 添加或修改
// 下面这句话表示,如果存在 key 就是修改数据,否则就是创建新的 key 和数据
map1[100] = "xxx"

// 5. 删除 map 中的数据
delete(map1, 100)

// 6. map 的大小
fmt.Println(len(map1)) // 1

遍历 Map

map 是无序的,每次打印出来的值可能都不一样,它不能通过 index 获取,只能通过 key 来获取。

map 的长度是不固定的,是引用类型。

len 可以用于 map 查看 map 中数据的数量,但是 cap 无法使用。

map 的 key 可以是布尔类型、整数、浮点数、字符串。

1
2
3
4
var map1 = map[string]int{"Go": 50, "Java": 89, "C": 87, "Python": 90}
for k, v := range map1 {
fmt.Println(k, v)
}

将 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
// 1. 创建 3 个人对象
user1 := make(map[string]string)
user1["name"] = "张飞"
user1["age"] = "19"

user2 := make(map[string]string)
user2["name"] = "吕布"
user2["age"] = "32"

user3 := map[string]string{"name": "赵云", "age": "88"}

// 2. 创建有 Map 组成的切片
// 默认长度=0,容量=3,没有问题!若指定长度等于 3,否则会添加三个空数据
userDatas := make([]map[string]string, 0, 3)

// 3. 扩容切片
userDatas = append(userDatas, user1)
userDatas = append(userDatas, user2)
userDatas = append(userDatas, user3)

// 4. 遍历切片,输出一个一个的 Map
for _, user := range userDatas {
fmt.Println(user)
}

学生管理版本 3(切片里面装 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
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
package main

import "fmt"

func main() {
var stus []map[string]string

fmt.Println(`
1. 查看学生
2. 添加学生
3. 删除学生
4. 修改学生
5. 退出系统
`)

for {
fmt.Print("请输入你的选择:")
var choice int
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Println("=====查看学生=====")
for i, v := range stus {
fmt.Printf("%d. 姓名:%s 年龄:%s 性别:%s\n", i+1, v["name"], v["age"], v["gender"])
}
case 2:
fmt.Println("=====添加学生=====")
fmt.Print("请输入学生姓名:")
var name string
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
var age string
fmt.Scanln(&age)
fmt.Print("请输入学生性别:")
var gender string
fmt.Scanln(&gender)
stus = append(stus, map[string]string{"name": name, "age": age, "gender": gender})
fmt.Println("添加成功")
case 3:
fmt.Println("=====删除学生=====")
fmt.Print("请输入学生姓名:")
var name string
fmt.Scanln(&name)
for i, v := range stus {
if v["name"] == name {
stus = append(stus[:i], stus[i+1:]...)
fmt.Println("删除成功")
break
}
}
case 4:
fmt.Println("=====修改学生=====")
fmt.Print("请输入要修改的学生姓名:")
var name string
fmt.Scanln(&name)
for i, v := range stus {
if v["name"] == name {
fmt.Print("请输入学生新名:")
var name string
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
var age string
fmt.Scanln(&age)
fmt.Print("请输入学生性别:")
var gender string
fmt.Scanln(&gender)
stus[i] = map[string]string{"name": name, "age": age, "gender": gender}
fmt.Println("修改成功")
break
}
}
case 5:
fmt.Println("=====退出系统=====")
// os.Exit(200)
return
default:
fmt.Println("输入错误,请重新输入")
}
}
}

Map 嵌套 Map

1
2
3
4
5
6
7
var stu1 = map[string]string{"name": "张飞", "age": "20", "address": "河南"}
var stu2 = map[string]string{"name": "潘凤", "age": "18", "address": "深圳"}
// key => int
// value => map[string]string
var stus = map[int]map[string]string{1000: stu1, 1001: stu2}

fmt.Println(stus[1001]["address"]) // 深圳

Map 嵌套切片

1
2
3
4
5
6
7
8
// key => string
// value => interface{} => 任意类型
var stu1 = map[string]interface{}{"name": "张飞", "age": "20", "address": "河南", "hobby": []string{"吃饭", "睡觉", "打豆豆"}}
// key => int
// value => map[string]string
var stus = map[int]map[string]interface{}{1000: stu1}
// 断言 => ([]string)[1]
fmt.Println(stus[1000]["hobby"].([]string)[1]) // 睡觉

函数

Go 代码中至少要有一个 main 入口函数。

声明和调用

1
2
3
4
func 函数名(形式参数 形参类型) (返回值类型) {
函数体
return 返回值 // 函数终止语句
}

求和函数。

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

func calc(num int) int {
var r = 0
for i := 1; i <= num; i++ {
r += i
}
return r
}

func main() {
r := calc(100)
println(r)
}

可变的参数

求任意多个实参的和。

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

func main() {
r := getSum(1, 2)
println(r)
}

// 如果也有位置参数,那么不定长参数要一致在最后
func getSum(nums ...int) int {
// nums 是一个 int 类型的切片
// fmt.Println(reflect.TypeOf(nums)) // []int
sum := 0
// nums 是一个切片
for i := 0; i < len(nums); i++ {
// 取出来
// sum = sum + nums[i]
sum += nums[i]
}
return sum
}

函数返回值

函数没有指定返回值,那么函数调用的结果不能被接收,否则会报错。

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

import "fmt"

func foo() {
}

func main() {
r := foo() // foo() (no value) used as value
fmt.Println(r)
}

举例,返回参数中大的那一个。

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

func main() {
r := getMax(1, 2)
println(r)
}

// int 类型的,比完大小之后我希望返回大的那一个数值
func getMax(num1 int, num2 int) int {
var result int
if num2 > num1 {
result = num2
} else {
result = num1
}
return result
}

函数可以有多个返回值。

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

func main() {
a, b := swip("张飞", "吕布")
println(a, b)
}

// 有多个返回值,后面需要用小括号包裹起来
func swip(x, y string) (string, string) {
return y, x
}

可以给返回值命名。

1
2
3
4
5
func foo() (ret int) {
ret = 10
// return ret
return // 简写
}
1
2
3
4
5
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return // 等价 return sum, sub
}

给返回值命名为一个变量后,可以在函数体内直接使用这个变量。

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

import "fmt"

func main() {
zc, area := calc(2, 4)
fmt.Println(zc, area)
}

func calc(len, wid float64) (area float64, zc float64) {
// 注意这儿是 =
// 面积 = 长 * 宽
area = len * wid
// 周长 = (长 + 宽)* 2
zc = (len + wid) * 2
// 函数的返回值的类型也可以命名,这两个的顺序无所谓
return zc, area
}

注意和下面的区分。

1
2
3
4
5
6
func calc(len, wid float64) (float64, float64) {
// 注意这儿是 :=
area := len * wid
zc := (len + wid) * 2
return zc, area
}

函数作用域(函数内变量的作用域)

函数外面的称为全局变量。

写在 if、for、函数,或者函数的形参,都是局部变量。局部变量不能在全局调用。

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

import "fmt"

func main() {
if temp := 1; true {
println(temp)
}
// 这里是访问不到 temp 的
fmt.Println(temp)
}

函数的传参

普通值传递的情况。

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

import "fmt"

func foo(x int) {
x = 100
}

func main() {
var x = 10
foo(x)
fmt.Println(x) // 10
}

传递指针的情况。

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

import "fmt"

func foo(p *int) {
*p = 100
}
func main() {
var a = 10
var p *int = &a
foo(p)
fmt.Println(a) // 100
}

传递切片的情况。

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

import "fmt"

func foo(s []string) {
fmt.Printf("foo 函数中接收的 s 的地址是:%p\n", &s)
s[0] = "马超"
}

func main() {
var s = []string{"张飞", "吕布", "赵云"}
fmt.Printf("main 函数中的 s 的地址:%p\n", &s)
foo(s)
fmt.Println(s)
}

xxx (17)

匿名的函数

可以将匿名函数给变量。

1
2
3
4
5
6
7
var foo = func() {
fmt.Println("将军")
}

fmt.Println(reflect.TypeOf(foo)) // func()

foo() // 赋值调用调用

可以直接调用匿名函数。

1
2
3
4
5
6
7
8
9
10
// 自执行的匿名函数
(func() {
fmt.Println("潘凤")
})()

// 匿名函数调用的结果给变量
var foo = (func(x, y int) int {
return x + y
})(1, 2)
fmt.Println(foo) // 3

函数内部不能声明普通函数,可以声明匿名函数。

1
2
3
4
5
6
package main

func main() {
// (func foo() {})() // 错误
(func() {})()
}

高阶函数

函数的参数是一个函数。

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

import "fmt"

// 回调函数
func main() {
// 高阶函数调用
r2 := oper(1, 2, add)
fmt.Println(r2) // 3

r3 := oper(1, 2, sub)
fmt.Println(r3) // -1
}

func oper(a, b int, fun func(int, int) int) int {
r := fun(a, b)
return r
}

func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}

递归函数

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

import "fmt"

func main() {
r := getSum(5)
fmt.Println(r)
}
func getSum(n int) int {
if n == 1 {
return 1
}
return getSum(n-1) + n
}

关于闭包

基本操作

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"

func increment() func() int {
// 定义一个局部变量
i := 0
// 在外层函数内部定义一个匿名函数,给变量自增并返回
fun := func() int {
i++
return i
}
return fun
}

func main() {
// 调用 increment 后返回的是一个函数,还没有执行
foo := increment()
foo()
foo()
r1 := foo()
fmt.Println(r1) // 3

bar := increment()
bar()
bar()
r2 := bar()
fmt.Println(r2) // 3
}

形参也是局部变量

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

import "fmt"

func increment(i int) func() int {
fun := func() int {
i++
return i
}
return fun
}

func main() {
// 调用 increment 后返回的是一个函数,还没有执行
foo := increment(10)
foo()
foo()
r1 := foo()
fmt.Println(r1) // 13
}

计算函数运行时间

通过高阶函数

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

func foo() {
fmt.Println("foo 功能开始")
time.Sleep(3 * time.Second)
fmt.Println("foo 功能结束")
}

func bar() {
fmt.Println("bar 功能开始")
time.Sleep(2 * time.Second)
fmt.Println("bar 功能结束")
}

func timer(f func()) {
start := time.Now().Unix()
f()
end := time.Now().Unix()
fmt.Println("cost timer:", end-start)
}

func main() {
timer(foo)
timer(bar)
}

违背了原来的调用方式

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

func foo() {
fmt.Println("foo 功能开始")
time.Sleep(3 * time.Second)
fmt.Println("foo 功能结束")
}

func bar() {
fmt.Println("bar 功能开始")
time.Sleep(2 * time.Second)
fmt.Println("bar 功能结束")
}

func getTimer(f func()) func() {
// 装饰函数里面执行了传递过来的 f 函数
return func() {
start := time.Now().Unix()
f()
end := time.Now().Unix()
fmt.Println("cost timer:", end-start)
}
}

func main() {
foo := getTimer(foo)
foo()
bar := getTimer(bar)
bar()
}

关于 defer

作用:处理一些善后的问题,比如错误,文件、网络流关闭等。

基本使用

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

func main() {
文件.Open()
// 函数最后 return 的时候再走 defer 语句
defer 文件.Close()
// 读写操作
// ...
// ...
}

一个 defer

1
2
3
4
5
6
7
8
9
package main

import "fmt"

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

执行顺序

如果有多个 defer,先进后出(栈)。

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

import "fmt"

func main() {
defer f("1")
fmt.Println("2")
// 等其他执行完毕之后,再执行
defer f("3")
fmt.Println("4")
}

func f(s string) {
fmt.Println(s)
}

// 2 4 3 1

拷贝机制

下面的执行结果是什么?

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

import "fmt"

func main() {
foo := func() {
fmt.Println("foo1")
}
// 直接拷贝了上面的 foo 函数
defer foo()
foo = func() {
fmt.Println("foo2")
}
}

继续观察传递参数的表现。

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

import "fmt"

func main() {
n := 10
fmt.Println("main start n=", n)
// defer 的时候,传递过去的是那个瞬时状态
defer f(n)
n++
fmt.Println("main end n=", n)
}

func f(n int) {
fmt.Println("f 函数中 n=", n)
}

继续观察输出结果。

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

import "fmt"

func main() {
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++
}

继续观察

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

import "fmt"

func main() {
x := 10
defer func() {
// 闭包,GPT 解释,保留的是地址
fmt.Println(x)
}()
x++
}

执行时机

1
2
3
4
5
6
func foo() {
// 1. rval = 100
// 2. 执行 defer 语句,defer 就在 1 和 2 之间执行
// 3. return rval
return 100
}

常见考题

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

import "fmt"

func f1() int {
i := 5
defer func() {
// 形成闭包
i++
}()
// 把 i 的【值】拷贝给了 rval
// 1. rval = i
// 2. 执行 defer 里面的 i++,注意是 i++,不是 rval++
// 3. return rval
return i
}
func f2() *int {
i := 5
defer func() {
i++
fmt.Printf(":::%p\n", &i) // 0xc00000a0d8
}()
fmt.Printf(":::%p\n", &i) // 0xc00000a0d8
// 把 i 的【地址】拷贝给了 rval
// 1. rval = i
// 2. 执行 defer 里面的 i++
// 3. return rval
return &i
}

func f3() (result int) {
defer func() {
result++
}()
// 有返回值变量的时候,result 替换了 rval,说明 result 返回值变量被改成了 5
// 1. result = 5
// 注意和第一题的区别
// 2. defer 执行,result++
// 3. return result
return 5
}

func f4() (result int) {
defer func() {
result++
}()
// 和 return 一样
return result
}

func f5() (r int) {
t := 5
defer func() {
t = t + 1
}()

// 有返回值变量,r 替换了 rval
// 1. r = t
// 2. defer
// 3. return r,注意有返回值变量,return 的一定是返回值变量的值
return t
}

func f6() (r int) {
fmt.Println(&r)
defer func(r int) {
// 用的自己的 r,没有闭包
r = r + 1
fmt.Println(&r)
}(r)
// 0. defer func 声明,defer 函数的参数在声明时求值,而非执行时
// 1. r = 5
// 2. defer 并没有修改外部的 r,defer 执行
// 3. return r
return 5
}

func f7() (r int) {
// 瞬时状态,拷贝机制,defer 时候的 r 是 0
defer func(x int) {
// 此时的 r 是外部的
r = x + 1
}(r)
// r = 5
// defer 修改了外部的 r
// return r
return 5
}

func main() {
println(f1())
println(*f2())
println(f3())
println(f4())
println(f5())
println(f6())
println(f7())
}

关于文件操作

字节与字符

一个字节等于八个二进制位,例如 uint8 表示无符号整型,占 8 位,也就是 2 的 8 次方,byte 和 unit8 类型本质上没有区别,它表示的是 ACSII 表中的一个字符。

byte 类型

1
2
3
4
5
var b byte
b = 'A' // 注意是单引号
fmt.Println(reflect.TypeOf(b)) // unit8
fmt.Println(b) // 65
fmt.Println(string(b)) // A

byte 等价于 uint8(无符号的 0 ~ 255)

1
2
3
4
var b uint8
b = 65
fmt.Println(b) // 65
fmt.Println(string(b)) // A

rune 类型

1
2
3
4
5
var b rune
b = '金'
fmt.Println(b) // 37329
fmt.Println(reflect.TypeOf(b)) // int32
fmt.Println(string(b)) // 金

rune 类型等价于 int32

1
2
3
4
5
var b int32
b = 37329
fmt.Println(b) // 37329
fmt.Println(reflect.TypeOf(b)) // int32
fmt.Println(string(b)) // 金

文件的读取

os.Open()

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

import (
"fmt"
"os"
)

func main() {
file1, err := os.Open("C:\\Users\\dangp\\Desktop\\gotest\\stus.json")
if err != nil {
fmt.Println(err)
}
fmt.Println(file1) // &{0xc0001206c8}
}

os.OpenFile()

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

import (
"fmt"
"os"
)

func main() {
file1, err := os.OpenFile("C:\\Users\\dangp\\Desktop\\gotest\\stus.json", os.O_RDONLY|os.O_WRONLY, os.ModePerm)
if err != nil {
fmt.Println(err)
}
fmt.Println(file1)
}

file.Read()

读取文件内容,file.Read()

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
// 1. 打开文件
file, _ := os.Open("D:\\Gogogo\\xxx.txt")
// 我们习惯于在建立连接时候通过 defer 来关闭连接,保证程序不会出任何问题,或者忘记关闭
defer file.Close()

// 2. 创建一个容器/切片 (二进制文本文件 => 读取流到一个容器 => 再读取容器里的数据)
// byte => uint
bs := make([]byte, 3, 1024)

// 3. 读取到缓冲区中,1 个汉字占 3 个字节
n, err := file.Read(bs)
fmt.Println(string(bs)) // 河
fmt.Println(n, err) // 3 <nil>

// 4. 继续读
n, err = file.Read(bs)
fmt.Println(string(bs)) // 南
fmt.Println(n, err) // 3 <nil>

// 为什么多读出了一个“南”
n, err = file.Read(bs)
fmt.Println(string(bs)) // 南

fmt.Println(n) // 0
fmt.Println(err) // EOF,读取到了文件末尾,就会返回 EOF

为什么多读出了一个汉字?读取到最后的时候它不会自动清空切片里之前的数据,可以使用下面的办法进行输出。

1
fmt.Println(string(bs[:n]))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

func main() {
file1, err := os.Open("C:\\Users\\dangp\\Desktop\\gotest\\xxx.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file1.Close()

// 使用for循环读取文件内容
bs := make([]byte, 3)
for {
n, err := file1.Read(bs)
if err == io.EOF {
// 文件读取完毕
break
}
if err != nil {
fmt.Println("读取文件失败:", err)
break
}
// 只打印实际读取的字节数
fmt.Printf("读取了 %d 个字节: %s\n", n, string(bs[:n]))
}
}

bufio.NewReader(file1).ReadLine()

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 (
"bufio"
"fmt"
"io"
"os"
)

func main() {
file1, err := os.Open("C:\\Users\\dangp\\Desktop\\gotest\\xxx.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file1.Close()

// 使用 bufio.NewReader 读取文件内容
reader := bufio.NewReader(file1)

for {
line, isPrefix, err := reader.ReadLine()
if err == io.EOF {
// 文件读取完毕
break
}
if err != nil {
fmt.Println("读取文件失败:", err)
break
}
// 处理长行(如果isPrefix为true,说明这一行还没有读完)
if isPrefix {
fmt.Printf("读取长行的一部分: %s\n", string(line))
// 继续读取这一行的剩余部分
for isPrefix {
line, isPrefix, err = reader.ReadLine()
if err != nil {
break
}
fmt.Printf("长行继续: %s\n", string(line))
}
} else {
fmt.Printf("读取完整行: %s\n", string(line))
}
}
}

bufio.NewReader(file1).ReadString(‘\n’)

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 (
"bufio"
"fmt"
"io"
"os"
)

func main() {
file1, err := os.Open("C:\\Users\\dangp\\Desktop\\gotest\\xxx.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file1.Close()

// 使用 bufio.NewReader 读取文件内容
reader := bufio.NewReader(file1)

for {
line, err := reader.ReadString('\n')
fmt.Print(line)
if err == io.EOF {
break
}
}
}

bufio.NewScanner(file)

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

func main() {
file1, err := os.Open("C:\\Users\\dangp\\Desktop\\gotest\\xxx.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file1.Close()

scanner := bufio.NewScanner(file1)

// 使用for循环读取所有行
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}

// 检查扫描过程中是否有错误
if err := scanner.Err(); err != nil {
fmt.Println("读取文件时发生错误:", err)
}
}

文件的写入

1
2
// 字符串和字节的转换
[]byte(str)

file.Write

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() {
fileName := "xxx.txt"
// 向一个文件中追加内容,如果没有,就是从头开始写
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println(err)
}
defer file.Close()
bs := []byte{65, 66, 67, 68, 69} // A B C D E
n, err := file.Write(bs)
if err != nil {
fmt.Println(err)
}
fmt.Println(n) // 5 字节
}

file.WriteString

1
2
3
4
5
6
7
fileName := "xxx.txt"
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println(err)
}
defer file.Close()
file.WriteString("哈哈哈哈哈哈哈") // 21 个字节

序列化(Map 类型的切片)

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

import (
"encoding/json"
"fmt"
"os"
)

func main() {
var stu1 = map[string]string{"name": "张飞", "age": "18", "address": "北京"}
var stu2 = map[string]string{"name": "吕布", "age": "28", "address": "河南"}
var stus = []map[string]string{stu1, stu2}

// 序列化 => 得到的是 JSON 格式的字符串 => 一般用来存储或网络传输
res, _ := json.Marshal(stus)
fmt.Println(string(res)) // [{"address":"北京","age":"18","name":"张飞"},{"address":"河南","age":"28","name":"吕布"}]

os.WriteFile("stus.json", res, 0666)
}

反序列化

1
2
3
4
5
res, _ := os.ReadFile("stus.json")
// 反序列化
var data []map[string]string
json.Unmarshal(res, &data)
fmt.Println(data)

文件信息

1
2
3
4
5
6
7
8
9
fileInfo, err := os.Stat("D:\\Gogogo\\stus.json")
if err != nil {
return
}
fmt.Println(fileInfo.Name()) // stus.json
fmt.Println(fileInfo.IsDir()) // false
fmt.Println(fileInfo.ModTime()) // 2025-04-12 23:31:03.1502448 +0800 CST
fmt.Println(fileInfo.Size()) // 97 字节
fmt.Println(fileInfo.Mode()) // -rw-rw-rw-,r可读、w可写、x可执行

还有一种表示权限的标识:八进制,例如 chomd 7 7 7

目录的创建和删除

存在我就打开,不存在就创建这个文件夹。

1
2
3
4
5
6
7
// 0o777 => 表示可读可写可执行
err := os.Mkdir("D:\\Gogogo\\ifer", os.ModePerm)
if err != nil {
// 存在就无法创建了,Cannot create a file when that file already exists.
fmt.Println(err)
}
fmt.Println("文件夹创建完毕")

创建多层文件夹。

1
2
3
4
5
err2 := os.MkdirAll("D:\\Gogogo\\ifer\\a\\b\\c", os.ModePerm)
if err2 != nil {
fmt.Println(err2)
}
fmt.Println("层级文件夹创建完毕")

删除空文件夹,通过 remove 方法只能删除单个/一层空文件夹。

1
2
3
4
5
err3 := os.Remove("D:\\Gogogo\\ifer\\a\\b\\c")
if err3 != nil {
fmt.Println(err3)
}
fmt.Println("file delete success!!")

如果存在多层文件,removeAll,相对来说比较危险,删除这个目录下的所有东西,强制删除。

1
2
3
4
5
err4 := os.RemoveAll("D:\\Gogogo\\ifer\\a")
if err4 != nil {
fmt.Println(err4)
}
fmt.Println("file delete success!!")

创建和删除文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 返回的 file 对象就是我们的文件
file1, err := os.Create("a.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(file1)
// 只有关闭了文件,才能被删除(否则文件会处于打开占用状态)
err = file1.Close()
if err != nil {
fmt.Println(err)
return
}
// 2. 删除文件
err = os.Remove("a.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("文件删除成功")

拷贝文件

io.Copy

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
source := "D:\\Gogogo\\a.txt"
dest := "D:\\Gogogo\\b.txt"

// 1. 打开源文件
sourceFile, err := os.Open(source)
if err != nil {
fmt.Printf("打开源文件失败: %v\n", err)
return
}
defer sourceFile.Close()

// 2. 创建目标文件
destFile, err := os.Create(dest)
if err != nil {
fmt.Printf("创建目标文件失败: %v\n", err)
return
}
defer destFile.Close()

// 3. 使用 io.Copy 把源文件拷贝到目标文件
_, err = io.Copy(destFile, sourceFile)
if err != nil {
fmt.Printf("文件拷贝失败: %v\n", err)
return
}

fmt.Println("文件拷贝成功!")

重要的结构体

先定义结构体

通过 type 和 struct 关键字声明结构体,格式如下:

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

举例:

1
2
3
4
5
6
type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

实例化结构体

[!NOTE]

引用类型:切片(Slice)、映射(Map)、通道(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
package main

import (
"fmt"
)

type Student struct {
sid int
name, address string
age int8
course []string
}

func initStu(s Student) Student {
s.age = 19
return s
}

func main() {
stu := Student{
sid: 1,
name: "张三",
address: "北京",
age: 18,
course: []string{"数学", "语文", "英语"},
}
initStu(stu)
fmt.Println(stu.age) // 18
}
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 {
sid int
name, address string
age int8
course []string
}

func initStu(s *Student) {
// (*s).age = 19
s.age = 19
}

func main() {
stu := Student{
sid: 1,
name: "张三",
address: "北京",
age: 18,
course: []string{"数学", "语文", "英语"},
}
initStu(&stu)
fmt.Println(stu.age) // 19
}

引用类型:切片(Slice)、映射(Map)、通道(Channel)、指针(Pointer)、函数(Function)、接口(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
package main

import "fmt"

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func main() {
var s1 Student // 或者 var s1 = Student{} 或 s1 := Student{}
fmt.Println(s1) // {0 0 []}

s1.sid = 1
s1.name = "IFER"
s1.address = "河南"
s1.age = 18
s1.course = []string{"语文", "数学"}

fmt.Println(s1) // {1 IFER 河南 18 [语文 数学]}

fmt.Printf("%p\n", &s1) // 0xc000092000
fmt.Printf("%p\n", &s1.sid) // 0xc000092000
}

声明赋值一起

1
2
3
4
5
// var s1 = Student{sid: 1, name: "IFER", address: "河南", age: 18, course: []string{"语文", "数学"}}
s1 := Student{sid: 1, name: "IFER", address: "河南", age: 18, course: []string{"语文", "数学"}}
// 也可以不需要 key,但要一一对应
// s1 := Student{1, "IFER", "河南", 18, []string{"语文", "数学"}}
fmt.Println(s1)

通过 new 操作符

new 操作符的返回值是结构体指针。

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

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func main() {
var s1 = new(Student)
fmt.Println(reflect.TypeOf(s1)) // *main.Student
(*s1).sid = 1
// 下面直接写 s1 是一种语法糖
s1.name = "IFER"
s1.address = "河南"
s1.age = 18
s1.course = []string{"语文", "数学"}
fmt.Println(*s1) // {1 IFER 河南 18 [语文 数学]}
}

函数传值之结构体

结构体是值类型的体现。

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"

// 1. 定一个结构体
type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func main() {
// 2. 结构体类型
stu1 := Student{1, "IFER", "河南", 18, []string{"语文", "数学"}}
fmt.Printf("%T,%p\n", stu1, &stu1) // main.User,0xc00001a180

// 3. 结构体是值类型的
// stu2 和 stu1 不是同一个地址
stu2 := stu1
fmt.Println(stu2)
fmt.Printf("%T,%p\n", stu2, &stu2) // main.User,0xc00001a210

stu2.name = "ELSER"
// 4. stu2 的修改不会影响 stu1
fmt.Println(stu1.name) // IFER
}

函数参数传递。

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

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func ageInit(s Student) {
s.age = 0
}
func main() {
var s1 = Student{sid: 1, name: "IFER", address: "河南", age: 18, course: []string{"语文", "数学"}}
// 结构体是值类型,按值传递,所以函数内部的修改不会影响外部
ageInit(s1)
fmt.Println(s1.age) // 18
}

如何得到结构体的指针?

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

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func ageInit(s Student) {
s.age = 0
}
func main() {
var s = &Student{sid: 1, name: "IFER", address: "河南", age: 18, course: []string{"语文", "数学"}}
fmt.Println(reflect.TypeOf(s)) // *main.Student
fmt.Println((*s).name) // IFER
// 语法糖
fmt.Println(s.name) // IFER
ageInit(*s)
fmt.Println((*s).age)
}

函数传参,如何传递结构体的指针?

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

import (
"fmt"
)

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func ageInit(s *Student) {
s.age = 0
}
func main() {
var s = &Student{sid: 1, name: "IFER", address: "河南", age: 18, course: []string{"语文", "数学"}}
ageInit(s)
fmt.Println(s.age) // 0
}

模拟构造函数

封装一个函数,返回结构体?

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 {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func NewStudent(sid int, name, address string, age int8, course []string) Student {
return Student{sid, name, address, age, course}
}

func main() {
var s = NewStudent(1, "IFER", "河南", 18, []string{"语文", "数学"})
fmt.Println(s) // {1 IFER 河南 18 [语文 数学]}
}

封装一个函数,返回结构体指针?

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

import (
"fmt"
)

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func NewStudent(sid int, name, address string, age int8, course []string) *Student {
return &Student{sid, name, address, age, course}
}

func main() {
var s = NewStudent(1, "IFER", "河南", 18, []string{"语文", "数学"})
fmt.Println(*s) // {1 IFER 河南 18 [语文 数学]}
}

结构体的方法

如何挂载方法

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

import "fmt"

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func NewStudent(sid int, name, address string, age int8, course []string) Student {
return Student{sid, name, address, age, course}
}

func (s Student) ageInit() {
// s => s1
s.age = 0
}
func main() {
var s1 = NewStudent(1, "IFER", "河南", 18, []string{"语文", "数学"})
s1.ageInit()
fmt.Println(s1.age) // 18
}

接收结构体指针

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

import "fmt"

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func NewStudent(sid int, name, address string, age int8, course []string) *Student {
return &Student{sid, name, address, age, course}
}

func (s *Student) ageInit() {
// s => s1
s.age = 0
}

func main() {
var s1 = NewStudent(1, "IFER", "河南", 18, []string{"语文", "数学"})
// 注意这里进行了简写,完整写法应该是 *s1
s1.ageInit()
fmt.Println(s1.age) // 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
package main

import "fmt"

type Student struct {
sid int
name, address string // 同类型的可以写一行,用逗号隔开
age int8
course []string
}

func NewStudent(sid int, name, address string, age int8, course []string) Student {
return Student{sid, name, address, age, course}
}

func (s *Student) ageInit() {
// s1 虽然是结构体,Go 语言会自动将 s1 转换为指针传递给方法,所以最终还是能够正确修改 s1 的 age 字段
// s => s1
// s 是谁,就看是谁调用的 ageInit 方法
s.age = 0
}

func main() {
var s1 = NewStudent(1, "IFER", "河南", 18, []string{"语文", "数学"})
s1.ageInit()
fmt.Println(s1.age) // 0
}

关于匿名字段

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

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

import "fmt"

type Student struct {
string
int
}

func main() {
s := Student{
"IFER",
18,
}
fmt.Printf("%#v\n", s) // main.Student{string:"IFER", int:18}
fmt.Println(s.string, s.int) // IFER 18
}

结构体本身也可以作为匿名字段使用。

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 Address struct {
country string
province string
city string
}

type Student struct {
name string
age int
Address
}

func main() {
s := Student{
"IFER",
18,
Address{"中国", "河南省", "鹤壁"},
}
fmt.Printf("%#v\n", s) // main.Student{name:"IFER", age:18, Address:main.Address{country:"中国", province:"河南省", city:"鹤壁"}}
fmt.Println(s.name, s.age) // IFER 18
fmt.Println(s.Address) // {中国 河南省 鹤壁}
fmt.Println(s.Address.country) // 中国
// 中间可以省略匿名字段 .Address
fmt.Println(s.city) // 鹤壁
}

通过嵌套匿名结构体实现继承

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

import "fmt"

// #1 Animal 动物
type Animal struct {
name string
}

func (a Animal) eat() {
fmt.Printf("%s is eating!\n", a.name)
}
func (a Animal) sleep() {
fmt.Printf("%s is sleeping!\n", a.name)
}

// #2 Dog 类型
type Dog struct {
Kind string
Animal // 通过嵌套匿名结构体实现继承
}

func (d *Dog) bark() {
fmt.Printf("%s is barking ~\n", d.name)
}

func main() {
// #3
d1 := Dog{
Kind: "金毛",
Animal: Animal{
name: "旺财",
},
}
d1.eat()
d1.bark()
}

另一种指针的写法:

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"

// #1 Animal 动物
type Animal struct {
name string
}

func (a *Animal) eat() {
fmt.Printf("%s is eating!\n", a.name)
}
func (a *Animal) sleep() {
fmt.Printf("%s is sleeping!\n", a.name)
}

// #2 Dog 类型
type Dog struct {
Kind string
*Animal // 通过嵌套匿名结构体实现继承,会影响实例化的方式,必须和这儿保持一致,也使用指针
}

func (d *Dog) bark() {
fmt.Printf("%s is barking ~\n", d.name)
}

func main() {
// #3
d1 := &Dog{
Kind: "金毛",
Animal: &Animal{
name: "旺财",
},
}
d1.eat()
d1.bark()
}

序列化(结构体)

序列化

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

type Addr struct {
Province string
City string
}

type User struct {
// key 的首字母大写才会被序列化,如果序列化的结果希望小写,则需要使用 json 标签
Name string `json:"name"`
Age int `json:"age"` // 忽略字段
Addr Addr `json:"addr"`
}

func main() {
var user1 = User{Name: "张飞", Age: 18, Addr: Addr{Province: "河南", City: "郑州"}}
var user2 = User{Name: "吕布", Age: 22, Addr: Addr{Province: "广州", City: "深圳"}}

// []User{user1, user2} => 切片
res, _ := json.Marshal([]User{user1, user2})
os.WriteFile("stus.json", res, 0666)
}

反序列化

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

type Addr struct {
Province string
City string
}

type User struct {
// key 的首字母大写才会被序列化,如果序列化的结果希望小写,则需要使用 json 标签
Name string `json:"name"`
Age int `json:"age"` // 忽略字段
Addr Addr `json:"addr"`
}

func main() {
jsonBytes, _ := os.ReadFile("stus.json")

/* var users []map[string]interface{}
json.Unmarshal(jsonBytes, &users)
fmt.Println(users[0]["name"]) */
var users []User
json.Unmarshal(jsonBytes, &users)
fmt.Println(users[0].Name)
}

学生管理作业

相关提示

核心:for 循环 + 接收用户输入 + 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
package main

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Gender int
Email string
Phone string
}

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
case 2:
// 查询所有学生
case 3:
// 查询某个学生
case 4:
// 修改学生
case 5:
// 删除学生
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

增加功能

核心:结构体类型的切片 + 通过 append 往切片里面添加结构体。

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

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体,Student{} 表示结构体的实例化
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
fmt.Println(students)
case 2:
// 查询所有学生
case 3:
// 查询某个学生
case 4:
// 修改学生
case 5:
// 删除学生
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
fmt.Println(students)
case 2:
// 查询所有学生
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
case 3:
// 查询某个学生
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
for _, student := range students {
if student.ID == id {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
break
}
}
case 4:
// 修改学生
case 5:
// 删除学生
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
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
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
package main

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
}

func getStudent() {
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

func getStudentByID() {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
/* for _, student := range students {
if student.ID == id {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
break
}
} */
// 如果逻辑比较多的话,可以抽出来放到下面处理
var index = -1
for i, student := range students {
if student.ID == id {
index = i
break
}
}
if index != -1 {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", students[index].ID, students[index].Name, students[index].Age, students[index].Email, students[index].Phone)
return
}
fmt.Println("没有找到该学生!")
}

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
addStudent()
case 2:
// 查询所有学生
getStudent()
case 3:
// 查询某个学生
getStudentByID()
case 4:
// 修改学生
case 5:
// 删除学生
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

修改学生

核心:封装 getIndexById 函数。

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package main

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
}

func getStudent() {
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// 函数的返回值的类型要保持统一
// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range students {
if student.ID == id {
index = i
break
}
}
return
}

func getOneStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func updateStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
addStudent()
case 2:
// 查询所有学生
getStudent()
case 3:
// 查询某个学生
getOneStudent()
case 4:
// 修改学生
updateStudent()
case 5:
// 删除学生
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

删除学生

核心:students = append(students[:index], students[index+1:]...)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package main

import "fmt"

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
}

func getStudent() {
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range students {
if student.ID == id {
index = i
break
}
}
return
}

func getOneStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func updateStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func delStudentByIndex() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
students = append(students[:index], students[index+1:]...)
fmt.Println("删除成功")
}

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
addStudent()
case 2:
// 查询所有学生
getStudent()
case 3:
// 查询某个学生
getOneStudent()
case 4:
// 修改学生
updateStudent()
case 5:
// 删除学生
delStudentByIndex()
case 6:
// 保存信息
case 7:
fmt.Println("退出系统")
default:
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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package main

import (
"encoding/json"
"fmt"
"os"
)

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
}

func getStudent() {
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range students {
if student.ID == id {
index = i
break
}
}
return
}

func getOneStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func updateStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func delStudentByIndex() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
students = append(students[:index], students[index+1:]...)
fmt.Println("删除成功")
}

func saveStudent() {
// 序列化 students 结构体
data, err := json.Marshal(students)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}
err = os.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
fmt.Println("保存成功")
}

func main() {
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
addStudent()
case 2:
// 查询所有学生
getStudent()
case 3:
// 查询某个学生
getOneStudent()
case 4:
// 修改学生
updateStudent()
case 5:
// 删除学生
delStudentByIndex()
case 6:
// 保存信息
saveStudent()
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

数据初始化

核心:读取数据到 jsonBytes,然后 json.Unmarshal(jsonBytes, &students) 反序列化。

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

var students []Student

func addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
students = append(students, Student{len(students) + 1, name, age, email, phone})
}

func getStudent() {
for _, student := range students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range students {
if student.ID == id {
index = i
break
}
}
return
}

func getOneStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func updateStudent() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func delStudentByIndex() {
index := getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
students = append(students[:index], students[index+1:]...)
fmt.Println("删除成功")
}

func saveStudent() {
// 序列化 students 结构体
data, err := json.Marshal(students)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}
err = os.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
fmt.Println("保存成功")
}

func initStudent() {
jsonBytes, _ := ioutil.ReadFile("students.json")
if len(jsonBytes) == 0 {
return
}
err := json.Unmarshal(jsonBytes, &students)
if err != nil {
fmt.Println("无法读取文件:", err)
return
}
}

func main() {
initStudent()
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
addStudent()
case 2:
// 查询所有学生
getStudent()
case 3:
// 查询某个学生
getOneStudent()
case 4:
// 修改学生
updateStudent()
case 5:
// 删除学生
delStudentByIndex()
case 6:
// 保存信息
saveStudent()
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

面向对象

核心:准备 StuService 结构体,其他的方法都挂载到这个结构体上面。

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main

import (
"encoding/json"
"fmt"
"os"
)

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

type StuService struct {
students []Student
}

func (s StuService) addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
s.students = append(s.students, Student{ID: len(s.students) + 1, Name: name, Age: age, Email: email, Phone: phone})
}

func (s StuService) getAllStudent() {
for _, student := range s.students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func (s StuService) getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range s.students {
if student.ID == id {
index = i
break
}
}
return
}

func (s StuService) getOneStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := s.students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func (s StuService) updateStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
s.students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
s.students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
s.students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
s.students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func (s StuService) delStudentByIndex() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
s.students = append(s.students[:index], s.students[index+1:]...)
fmt.Println("删除成功")
}

func (s StuService) saveStudent() {
data, err := json.Marshal(s.students)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}

err = os.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
// 这里可以实现保存学生信息到文件的功能
fmt.Println("保存成功")
}

func (s StuService) initStudent() {
jsonBytes, _ := os.ReadFile("students.json")
if len(jsonBytes) == 0 {
fmt.Println("没有找到学生信息文件")
return
}
err := json.Unmarshal(jsonBytes, &s.students)
if err != nil {
fmt.Println("无法读取文件:", err)
return
}
}

func main() {
stu := StuService{}
stu.initStudent()
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
stu.addStudent()
case 2:
// 查询所有学生
stu.getAllStudent()
case 3:
// 查询某个学生
stu.getOneStudent()
case 4:
// 修改学生
stu.updateStudent()
case 5:
// 删除学生
stu.delStudentByIndex()
case 6:
// 保存信息
stu.saveStudent()
case 7:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

问题解决

为什么添加完学生后看不到?

答案:结构体是值类型,像下面每次调用方法的时候发生了值拷贝。

1
2
3
4
5
6
stu.addStudent() // 这个方法里面的 stu
stu.getAllStudent() // 和这个方法里面的 stu 不是同一个

// 解决:这个 & 可以省略,一般加上更符合直觉
stu := &StuService{}
func (s *StuService) addStudent() {}
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main

import (
"encoding/json"
"fmt"
"os"
)

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

type StuService struct {
students []Student
}

func (s *StuService) addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
s.students = append(s.students, Student{ID: len(s.students) + 1, Name: name, Age: age, Email: email, Phone: phone})
}

func (s *StuService) getAllStudent() {
for _, student := range s.students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func (s *StuService) getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range s.students {
if student.ID == id {
index = i
break
}
}
return
}

func (s *StuService) getOneStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := s.students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func (s *StuService) updateStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
s.students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
s.students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
s.students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
s.students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func (s *StuService) delStudentByIndex() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
s.students = append(s.students[:index], s.students[index+1:]...)
fmt.Println("删除成功")
}

func (s *StuService) saveStudent() {
data, err := json.Marshal(s.students)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}
err = os.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
// 这里可以实现保存学生信息到文件的功能
fmt.Println("保存成功")
}

func (s *StuService) initStudent() {
jsonBytes, _ := os.ReadFile("students.json")
if len(jsonBytes) == 0 {
fmt.Println("没有找到学生信息文件")
return
}
err := json.Unmarshal(jsonBytes, &s.students)
if err != nil {
fmt.Println("无法读取文件:", err)
return
}
}

func main() {
// & 也可以不写
stu := &StuService{}
stu.initStudent()
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
stu.addStudent()
case 2:
// 查询所有学生
stu.getAllStudent()
case 3:
// 查询某个学生
stu.getOneStudent()
case 4:
// 修改学生
stu.updateStudent()
case 5:
// 删除学生
stu.delStudentByIndex()
case 6:
// 保存信息
stu.saveStudent()
case 7:
fmt.Println("退出系统")
default:
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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package main

import (
"encoding/json"
"fmt"
"os"
)

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

type StuService struct {
students []Student
}

func (s *StuService) addStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
s.students = append(s.students, Student{ID: len(s.students) + 1, Name: name, Age: age, Email: email, Phone: phone})
}

func (s *StuService) getAllStudent() {
for _, student := range s.students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func (s *StuService) getIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range s.students {
if student.ID == id {
index = i
break
}
}
return
}

func (s *StuService) getOneStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := s.students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func (s *StuService) updateStudent() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
s.students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
s.students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
s.students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
s.students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func (s *StuService) delStudentByIndex() {
index := s.getIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
s.students = append(s.students[:index], s.students[index+1:]...)
fmt.Println("删除成功")
}

type FileService struct {
}

func (f *FileService) write(fromData []Student) {
data, err := json.Marshal(fromData)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}

err = os.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
// 这里可以实现保存学生信息到文件的功能
fmt.Println("保存成功")
}

func (f *FileService) read() []Student {
var data []Student
jsonBytes, _ := os.ReadFile("students.json")
if len(jsonBytes) == 0 {
fmt.Println("没有找到学生信息文件")
return data
}
err := json.Unmarshal(jsonBytes, &data)
if err != nil {
fmt.Println("无法读取文件:", err)
}
return data
}

func main() {
stu := &StuService{}
file := &FileService{}
stu.students = file.read()
for {
fmt.Println(`
================================
==== 1. 添加学生
==== 2. 查询所有学生
==== 3. 查询某个学生
==== 4. 修改学生
==== 5. 删除学生
==== 6. 保存信息
==== 7. 退出系统
================================
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
stu.addStudent()
case 2:
// 查询所有学生
stu.getAllStudent()
case 3:
// 查询某个学生
stu.getOneStudent()
case 4:
// 修改学生
stu.updateStudent()
case 5:
// 删除学生
stu.delStudentByIndex()
case 6:
// 保存到文件
file.write(stu.students)
case 7:
// 保存到数据库
// 这里可以实现保存到数据库的功能
case 8:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

掌握接口使用

Go 语言提供了接口数据类型,接口就是把一些共性的方法集合在一起定义,不需要实现具体的方法内容。如果有结构体将接口定义的方法全部实现了,那么就代表实现了这个接口。Go 是隐式实现,A 结构体实现了 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
package main

import "fmt"

// 结构体 1
type AliPay struct{}

// 结构体 2
type WeixinPay struct{}

// 结构体 1 下面的 pay 方法
func (a AliPay) pay() {
fmt.Println("支付宝pay")
}

// 结构体 2 下面的 pay 方法
func (w WeixinPay) pay() {
fmt.Println("微信pay")
}

func main() {
// 结构体实例
var p = AliPay{}
p.pay() // 支付宝pay

var w = WeixinPay{}
w.pay()
}

接口演示。

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

import "fmt"

// 1. 定义接口
type Pay interface {
pay() string
}

// 2. 通过结构体实现接口
type AliPay struct{}
type WeixinPay struct{}

// 接口中的 pay 方法要求返回 string
func (a AliPay) pay() string {
fmt.Println("支付宝pay")
return "AliPay"
}

func (w WeixinPay) pay() string {
fmt.Println("微信pay")
return "WeixinPay"
}

func main() {
var p Pay
p = AliPay{}
p.pay()
// 可以做到重新赋值!多态
p = WeixinPay{}
p.pay()
}

结构体(struct)实现了接口的全部方法就代表实现了这个接口(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
57
58
59
package main

import (
"fmt"
)

// 1. 定义接口,方法定义的集合
type USB interface {
input() // 输入方法
output() // 输出方法
}

// 2. 定义结构体
type Mouse struct {
name string
}
// 3. 结构体实现了接口的全部方法就代表实现了这个接口
func (mouse Mouse) output() {
fmt.Println(mouse.name, "鼠标输出")
}
func (mouse Mouse) input() {
fmt.Println(mouse.name, "鼠标输入")
}

// 4. 接口测试
func test(u USB) {
u.input()
u.output()
}

func main() {
// 5. 初始化结构体
m1 := Mouse{name: "罗技"}
// test 函数的参数要求是 USB 类型
// 如果一个结构体实现了这个接口所有的方法,那这个结构体是符合这个接口类型的
// 6. 调用接受接口类型的方法
test(m1)

k1 := KeyBoard{name: "雷蛇"}
test(k1)

// 7. 定义高级类型,k1 从 KeyBoard 升级成了 USB,向上转型
var usb USB
usb = k1
fmt.Println(usb)
// 升级后的 usb 后只能使用 USB 的东西,是无法使用类/结构体的属性的
// fmt.Println(usb.name)
}

type KeyBoard struct {
name string
}

func (key KeyBoard) output() {
fmt.Println(key.name, "键盘输出")
}
func (key KeyBoard) input() {
fmt.Println(key.name, "键盘输入")
}

模拟多态

接口的实现类/结构体都拥有多态特性:除了自己本身还是他对应接口的类型。

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"

// 1. 定义接口
type Animal interface {
eat()
sleep()
}

// 2. 实现接口
type Dog struct {
name string
}

func (dog Dog) eat() {
fmt.Println(dog.name, "--eat")
}
func (dog Dog) sleep() {
fmt.Println(dog.name, "--sleep")
}

func main() {
// 3. Dog 两重身份(多态):Dog 和 Animal
dog := Dog{name: "旺财"}
dog.eat()
dog.sleep()

// 4. 第一种身份
foo(dog)

// 5. 定义一个类型可以为接口类型的变量,实际上所有实现类都可以赋值给这个对象
// 此时是模糊的,将具体的实现类赋值给他,才有意义
var animal Animal
animal = dog
// 第二种身份
foo(animal)
}

// Animal 接口
func foo(a Animal) {
a.eat()
a.sleep()
}

空接口

空接口就是 any

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

import "fmt"

// 1. 空接口,不包含任何方法,因此,可以认为所有的结构体都默认实现了空接口,所以空接口可以存储任何的类型
type A interface{}
type Dog struct {
name string
}
type Cat struct {
name string
}

func foo(a A) {
fmt.Println(a)
}

func main() {
var a1 A = Cat{name: "喵喵"}
var a2 A = Dog{name: "旺财"}
var a3 A = 1
var a4 A = "dva"
fmt.Println(a1)
fmt.Println(a2)
fmt.Println(a3)
fmt.Println(a4)
// 2. 调用方法
foo(a1)
foo(1)

// 3. 用控接口定义其他数据
// map,空接口就是 any
map1 := make(map[string]interface{})
map1["name"] = "dva"
map1["age"] = 18
fmt.Println(map1)

// slice
s1 := make([]any, 0, 10)
s1 = append(s1, 1, "12312", false, a1, a2)
fmt.Println(s1)
}

其他举例

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() {
// 定义一个空接口类型的 x
var x interface{}
s := "吕布"
x = s
fmt.Printf("type:%T value:%v\n", x, x)

i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)

b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}

接口嵌套

1
2
3
4
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"
)

type AA interface {
test1()
}
type BB interface {
test2()
}

// 1. 接口嵌套
type CC interface {
AA
BB
test3()
}

// 2. 编写一个结构体实现接口 CC
type Dog struct {
}

func (dog Dog) test1() {
fmt.Println("test1")
}
func (dog Dog) test2() {
fmt.Println("test2")
}
func (dog Dog) test3() {
fmt.Println("test3")
}

func main() {
// 3. dog 拥有 4 种形态: Dog 、CC 、 BB 、 AA
var dog Dog = Dog{}
dog.test1()
dog.test2()
dog.test3()

// 4. 向上转型之后只能调用它自己对应的方法
var a AA = dog
a.test1()
// a.test2() // 不能调用
var b BB = dog
b.test2()
var c CC = dog
c.test1()
c.test2()
c.test3()
}

接口断言

被断言的对象必须是接口类型,否则会报错。

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. 定义一个空接口
type I interface{}

// 2. 如果断言的类型同时实现了 switch 多个 case 匹配,默认使用第一个 case
func testAsserts(i interface{}) {
// switch i.(type) 接口断言,这儿是一种固定写法
switch i.(type) {

case string:
fmt.Println("变量为string类型")
case int:
fmt.Println("变量为int类型")
case I:
fmt.Println("变量为I类型")
case nil:
fmt.Println("变量为nil类型")
case interface{}:
fmt.Println("变量为interface{}类型")
default:
fmt.Println("未知类型")
}
}

func main() {
testAsserts("string")
testAsserts(1)
var i I // 默认值为 nil
var i2 I = 1 // 只有赋值了之后,才是对应的类型
testAsserts(i)
testAsserts(i2)
}

type 定义类型和起别名

定义类型

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

import "fmt"

// 1. 这里定义了一个新类型 MyInt,是 int 转换过来的,和 int 一样,但是不能通 int 发生操作,类型不同
type MyInt int

func main() {
var a MyInt = 20 // MyInt
var b int = 10 // int

// invalid operation: a + b (mismatched types MyInt and int)
// fmt.Println(a + b)
// 2. 类型转换: T(v)
fmt.Println(int(a) + b) // 30
fmt.Printf("%T\n", a) // main.MyInt
fmt.Printf("%T\n", b) // int
}

/*
1. type 定义一个类型
- type User struct 定义结构体类型
- type User interface 定义接口类型
- type Diy (int、string、....) 自定义类型,全新的类型

2. type 起别名
- type xxx = 类型 ,将某个类型赋值给 xxx,相当于这个类型的别名
- 别名只能在写代码的时候使用,增加代码的可阅读性
- 真实在项目的编译过程中,它还是原来的类型
*/

别名

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

import (
"fmt"
)

func main() {
type diyint = int
var a diyint = 30
var b int = 10
var c = a + b

fmt.Printf("%d\n", c)
}

接口应用

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

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

import "fmt"

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

func main() {
show("hello world")
show(1)
}

空接口作为 map 的值:使用空接口实现可以保存任意值的字典。

1
2
3
4
var student = make(map[string]interface{})
student["name"] = "张飞"
student["age"] = 18
fmt.Println(student)

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

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

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

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

学习包的管理

Go 程序首先在 GOROOT/src 目录中寻找包目录,如果没有找到,则会去 GOPATH/src 目录中继续找,比如 fmt 包是位于 GOROOT/src 目录的,那么它将会从该目录导入。导入包的时候,会从 GOPATH/src 去查找(注意,除了全局的环境变量,使用 GoLand 开发的时候也可以局部配置),需要使用包名.函数名来使用。main 函数所在的包,必须是 main 包,表示程序的入口。一个目录下所有的 Go 文件的 package 必须同名。

包分类:标准库、第三方、自定义。

GOPATH 下导包

可以通过 go env 查看 GOPATH 地址,我电脑的 GOPATH 是 C:\Users\dangp\go。

1
2
# 关闭 GOMOD
go env -w GO111MODULE=off

main.go 可以放在任何地方,用 VSCode 运行。

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

import (
"xuego/hello/api"
"xuego/hello/db"
)

func main() {
api.RestAPI1()
api.RestAPI2()
db.MysqlHandler()
db.RedisHandler()
}

C:\Users\dangp\go\src\xuego\hello\api\http.go

1
2
3
4
5
6
7
package api

import "fmt"

func RestAPI1() {
fmt.Println("restAPI1")
}

C:\Users\dangp\go\src\xuego\hello\api\rpc.go

1
2
3
4
5
6
7
8
package api

import "fmt"

func RestAPI2() {
fmt.Println("restAPI2")
}

C:\Users\dangp\go\src\xuego\hello\db\mysql.go

1
2
3
4
5
6
7
8
package db

import "fmt"

func MysqlHandler() {
fmt.Println("mysqlHandler")
}

C:\Users\dangp\go\src\xuego\hello\db\redis.go

1
2
3
4
5
6
7
package db

import "fmt"

func RedisHandler() {
fmt.Println("redisHandler")
}

导包的几种形式

1
2
3
4
5
6
import (
// "crypto/rand" // 正常导入
// R "math/rand" // 可以给包起别名
// . "math/rand" // 简便模式:可以直接调用该包下的函数,不需要通过包名点的形式
_ "math/rand" // 匿名导入,只会执行这个包下的 init 方法
)

init 函数,在 main 方法执行之前执行,通过 init 函数,通常用来初始化一些全局变量,建立一些第三方的连接(数据库连接)、注册、检查、修复程序状态。init 函数可以有多个。

1. 如果导入了多个匿名包,按照 main 中导入包的顺序来进行执行。
2. 同一个包下的多个 Go 文件,都有 init 的情况下,按照文件排放顺序来执行对应的 init 函数。
3. 单个 Go 文件中的多个 init 是顺序执行的。
4. 等所有 Go 文件中的 init 函数执行完毕后,才会到 main 包。

包加载顺序.drawio

重点掌握 GoMod

1
2
3
4
5
6
7
go env
go env -w GO111MODULE=on

go mod init # 模块路径

# 如果你的模块路径是 `xuego/hello`,那么在导包时首先要写上 'xuego/hello',后面再拼接上其他路径
# 如果你的模块路径是 `xxx`,那么在导包时,首先要写上 `xxx`,后面再拼接上其他路径

完成上面 GOPATH 的案例:

1
go mod init xuego
1
2
3
4
5
6
7
8
9
📦hello
┣ 📂api
┃ ┣ 📜http.go
┃ ┗ 📜rpc.go
┗ 📂db
┃ ┣ 📜mysql.go
┃ ┗ 📜redis.go
┣ go.mod
┣ main.go

设置包的代理

1
go env -w GOPROXY="https://goproxy.cn,direct"

如何更改模块路径

如果初始化时指定的模块路径不合适,可以随时修改。

1. 编辑 go.mod 文件,将 module 行改为新的模块路径。例如:

1
module github.com/username/xuego/hello

2. 更新项目中的所有导入路径,确保它们与新的模块路径一致。例如:

1
import "github.com/username/xuego/hello/api"

3. 运行以下命令更新依赖:

1
2
# 在 go.mod 所在的同级目录
go mod tidy

关于 go mod tidy

假如 main.go 中的代码如下:

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

import (
"fmt"
"github.com/jinzhu/now"
)

func main() {
fmt.Println(now.BeginningOfMinute())
}

我可以在 go.mod 所在的目录执行命令 go mod tidy 来拉取包中依赖的第三方包。第三方的依赖都会放到 GOMODCACHE 对应的目录,即 GOPATH\pkg\mod 目录。

1
2
# 让包记录到自己的项目,不再是 GOMODCACHE 啦,vendor 是固定写法
go mod vendor

内置的 strings 包

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

func main() {
// 1. 字符是不能修改的
str := "dva, xxx"
fmt.Println(str[0]) // 100

// 2. 判断某个字符是否包含了指定的内容
fmt.Println(strings.Contains(str, "x")) // true

// 3. 判断某个字符串是否包含了多个字符串中的某一个
fmt.Println(strings.ContainsAny(str, "xi")) // true

// 4. 统计这个字符在指定字符串中出现的数量 Count() 计数
fmt.Println(strings.Count(str, "x")) // 3

fileName := "20250301.mp3"
// 5. 判断用什么开头的
if strings.HasPrefix(fileName, "2025") {
fmt.Println("找到 2025 开头的文件:", fileName) // 找到 2025 开头的文件: 20250301.mp3
}
// 6. 判断用什么结尾的 HasSuffix()
if strings.HasSuffix(fileName, ".mp3") {
fmt.Println("找到 mp3 结尾的文件:", fileName) // 找到 mp3 结尾的文件: 20250301.mp3
}

// 7. 寻找这个字符串第一次出现的位置 Index()
fmt.Println(strings.Index(str, "v")) // 1,找不到会返回 -1

// 8. 寻找这个字符串最后一次出现的位置 LastIndex()
fmt.Println(strings.LastIndex(str, "a")) // 2

// 9. 拼接字符串, 数组或者切片拼接,前端给了我们多个参数
str2 := []string{"a", "b", "c", "d", "e"}
fmt.Println(strings.Join(str2, "~")) // a~b~c~d~e

// 10. 通过某个格式,拆分字符串 Split()
str3 := strings.Join(str2, "~") // [a~b~c~d~e]
fmt.Println(strings.Split(str3, "~")) // [a b c d e]

// 11. 大小写 ToUpper()
fmt.Println(strings.ToUpper(str)) // DVA, XXX
fmt.Println(strings.ToLower(str)) // dva, xxx

// 12. 替换
fmt.Println(strings.Replace(str, "x", "🤠", 1)) // dva, 🤠xx

// 13. 截取某个字符串
str5 := str[0:3]
fmt.Println(str5) // dva
}

strconv 类型转换

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

import (
"fmt"
"strconv"
)

func main() {
s1 := "true"
// 1. 字符串转 bool
b1, err := strconv.ParseBool(s1)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%T,%t\n", b1, b1) // bool,true

// 2. 格式化布尔为字符串
s2 := strconv.FormatBool(b1)
fmt.Printf("%T,%s\n", s2, s2) // string,true

// 3. 字符串转 int
s3 := "100000"
// 参数:str、源进制(10)、大小
i1, _ := strconv.ParseInt(s3, 10, 64)
fmt.Printf("%T,%d\n", i1, i1) // int64,100000

// 4. 格式化 int 为字符串
s4 := strconv.FormatInt(i1, 10)
fmt.Printf("%T,%s\n", s4, s4) // string,100000

// 5. 字符串转数字(十进制)
atoi, _ := strconv.Atoi("-20")
fmt.Printf("%T,%d\n", atoi, atoi) // int,-20

// 6. 数字转字符串
itoa := strconv.Itoa(30)
fmt.Printf("%T,%s\n", itoa, itoa) // string,30
}

常见内置包演示

time

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

func main() {
time1()
}

func time1() {
// 返回值为 Time 结构体
now := time.Now()

year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
// %02d 表示如果不足两位,左侧用 0 补齐输出
// 2025-03-01 15:12:31
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
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"
"time"
)

func main() {
time2()
}

func time2() {
now := time.Now()
// 时间格式化 2023-02-23 20:43:49
// 格式化模板:yyyy-MM-dd HH:mm:ss
// Go 语言诞生的时间作为格式化模板:2006 年 1 月 2 号下午 3 点 4 分
// Go 语言格式化时间的代码:2006-01-02 15:04:05 (记忆方式:2006 12 345)
// 固定的:"2006-01-02 15:04:05"
fmt.Println(now.Format("2006-01-02 15:04:05")) // 24 小时制
fmt.Println(now.Format("2006-01-02 03:04:05 PM")) // 12 小时制
fmt.Println(now.Format("2006/01/02 15:04")) // 2023/02/23 20:52
fmt.Println(now.Format("15:04 2006/01/02")) // 20:52 2023/02/23
fmt.Println(now.Format("2006/01/02")) // 2023/02/23
}
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"
"time"
)

func main() {
time3()
}

// 将字符串格式化为 Time 对象 (获取到网页传递的时间字符串,需要转化为 Time 才能在代码中使用)
func time3() {
// 其他地方的时区格式:https://www.zeitverschiebung.net/cn/all-time-zones.html
// Asia/Shanghai" 注意大小写,如果不对,会报未知的时间错误
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println(err)
return
}
// 将字符串解析为时间 Time
timeStr := "2025-04-01 15:28:33"
timeObj, _ := time.ParseInLocation("2006-01-02 15:04:05", timeStr, loc)
fmt.Println(timeObj)
}
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"
"time"
)

func main() {
time4()
}

// 时间戳:更多时候和随机数结合
func time4() {
// 格林威治时间自 1970 年 1 月 1 日(00:00:00 GMT) 至当前时间的总秒数
// 1970.1.1 00:00:00
now := time.Now()
timestamp1 := now.Unix() // 时间戳
timestamp2 := now.UnixNano() // 纳秒的时间数
fmt.Println(timestamp1) // 1740814216
fmt.Println(timestamp2) // 1740814216281495700
// 通过 Unix 转换 time 对象
timeObj := time.Unix(timestamp1, 0) // 返回 time 对象
year := timeObj.Year()
month := timeObj.Month()
day := timeObj.Day()
hour := timeObj.Hour()
minute := timeObj.Minute()
second := timeObj.Second()
// 2025-03-01 15:30:16
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}

随机数

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

import (
"fmt"
"math/rand"
)

func main() {
// 1. 获取一个随机数
num1 := rand.Int()
fmt.Println("num1:", num1) // 7621103522683197901

// 2. [0, 100)
num2 := rand.Intn(100)
fmt.Println("num2:", num2) // 67
}

定时器

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

import (
"fmt"
"time"
)

func main() {
// 每一秒都会触发
ticker := time.Tick(time.Second)
for i := range ticker {
fmt.Println(i)
}
}

时间比较

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

func main() {
// 加、减、比较(在 xxx 之前、在 xxx 之后、相等)
now := time.Now()
// 1. 加一个小时
later := now.Add(time.Hour)
//2025-03-01 16:43:32.3842393 +0800 CST m=+3600.000000001
fmt.Println(later)

// 2. later 和现在时间的差值
subTime := later.Sub(now)
fmt.Println(subTime)

// 3. 比较时间, 校验时间当地时间和网络时间是否一致
fmt.Println(now.Equal(later)) // fasle
fmt.Println(now.Before(later)) // true
fmt.Println(now.After(later)) // fasle
}

学生管理拆分包

main.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
. "cms/service" // 可以直接调用该包下的函数
"fmt"
)

func main() {
stu := &StudentService{}
file := &FileDBService{}
stu.Students = file.Read()
for {
fmt.Println(`
1. 添加学生
2. 查询所有学生
3. 查询某个学生
4. 修改学生
5. 删除学生
6. 保存到文件
7. 保存到数据库
8. 退出系统
`)

var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)

switch choice {
case 1:
// 添加学生
stu.AddStudent()
case 2:
// 查询所有学生
stu.GetAllStudent()
case 3:
// 查询某个学生
stu.GetOneStudent()
case 4:
// 修改学生
stu.UpdateStudent()
case 5:
// 删除学生
stu.DelStudentByIndex()
case 6:
// 保存到文件
file.Write(stu.Students)
case 7:
// 保存到数据库
// 这里可以实现保存到数据库的功能
case 8:
fmt.Println("退出系统")
default:
fmt.Println("无效的选择,请重新输入")
}
}
}

go.mod

1
2
3
module cms

go 1.23.4

service/db.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package service

import (
"cms/config"
. "cms/model"
"encoding/json"
"fmt"
"io/ioutil"
"os"
)

type FileDBService struct {
}

func (f *FileDBService) Write(fromData []Student) {
// 注意这里的 ./ 是相对于命令执行的目录的
file, err := os.Create(config.StudentJSONPath)
if err != nil {
fmt.Println("无法创建文件:", err)
return
}
defer file.Close()

data, err := json.Marshal(fromData)
if err != nil {
fmt.Println("无法转换为 JSON:", err)
return
}

_, err = file.Write(data)
if err != nil {
fmt.Println("无法写入文件:", err)
return
}
/* err = ioutil.WriteFile("students.json", data, 0666)
if err != nil {
fmt.Println("无法写入文件:", err)
return
} */
// 这里可以实现保存学生信息到文件的功能
fmt.Println("保存成功")
}

func (f *FileDBService) Read() []Student {
var data []Student
jsonBytes, _ := ioutil.ReadFile(config.StudentJSONPath)
if len(jsonBytes) == 0 {
fmt.Println("没有找到学生信息文件")
return data
}
err := json.Unmarshal(jsonBytes, &data)
if err != nil {
fmt.Println("无法读取文件:", err)
}
return data
}

service/student.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
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
108
109
110
111
112
113
package service

import (
. "cms/model"
"fmt"
)

type StudentService struct {
Students []Student
}

func (s *StudentService) AddStudent() {
var name string
var age int
var email string
var phone string
fmt.Print("请输入学生姓名:")
fmt.Scanln(&name)
fmt.Print("请输入学生年龄:")
fmt.Scanln(&age)
fmt.Print("请输入学生邮箱:")
fmt.Scanln(&email)
fmt.Print("请输入学生电话:")
fmt.Scanln(&phone)
// 通过键值生成结构体
// students = append(students, Student{ID: len(students) + 1, Name: name, Age: age, Email: email, Phone: phone})
// 通过顺序生成结构体
s.Students = append(s.Students, Student{ID: len(s.Students) + 1, Name: name, Age: age, Email: email, Phone: phone})
}

func (s *StudentService) GetAllStudent() {
for _, student := range s.Students {
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}
}

// (index int) 是返回值变量的语法,如果只写了 return,后面没有内容,则返回这个变量的值
func (s *StudentService) GetIndexById() (index int) {
var id int
fmt.Print("请输入学生ID:")
fmt.Scanln(&id)
// 如果逻辑比较多的话,可以抽出来放到下面处理
index = -1
for i, student := range s.Students {
if student.ID == id {
index = i
break
}
}
return
}

func (s *StudentService) GetOneStudent() {
index := s.GetIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
student := s.Students[index]
fmt.Printf("ID: %d, 姓名: %s, 年龄: %d, 邮箱: %s, 电话: %s\n", student.ID, student.Name, student.Age, student.Email, student.Phone)
}

func (s *StudentService) UpdateStudent() {
index := s.GetIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
fmt.Println(`
1. 修改姓名
2. 修改年龄
3. 修改邮箱
4. 修改电话
`)
var choice int
fmt.Print("请输入您的选择:")
fmt.Scanln(&choice)
switch choice {
case 1:
fmt.Print("请输入新的姓名:")
var name string
fmt.Scanln(&name)
s.Students[index].Name = name
case 2:
fmt.Print("请输入新的年龄:")
var age int
fmt.Scanln(&age)
s.Students[index].Age = age
case 3:
fmt.Print("请输入新的邮箱:")
var email string
fmt.Scanln(&email)
s.Students[index].Email = email
case 4:
fmt.Print("请输入新的电话:")
var phone string
fmt.Scanln(&phone)
s.Students[index].Phone = phone
default:
fmt.Println("无效的选择")
}
}

func (s *StudentService) DelStudentByIndex() {
index := s.GetIndexById()
if index == -1 {
fmt.Println("没有找到该学生")
return
}
s.Students = append(s.Students[:index], s.Students[index+1:]...)
fmt.Println("删除成功")
}

model/student.go

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

// 定义学生的结构体
type Student struct {
ID int
Name string
Age int
Email string
Phone string
}

db/students.json

1
2
3
4
5
6
7
8
9
[
{
"ID": 1,
"Name": "IFER",
"Age": 18,
"Email": "IFER@QQ.COM",
"Phone": "18598279121"
}
]

config/config.go

1
2
3
4
package config

var StudentJSONPath = "./db/students.json"

学习网络编程

基本操作

server/main.go,net.Listen

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

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

// 2. 会产生阻塞,直到客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(conn.RemoteAddr().String(), "有人连接成功")
}

client/main.go,net.Dial

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

import (
"fmt"
"net"
)

func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()

fmt.Println(conn.RemoteAddr().String(), "连接服务器连接成功")
}

继续处理

server/main.go,conn.Read 接收消息

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

import (
"fmt"
"net"
)

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

// 2. 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
data := make([]byte, 1024)

// 3. 接收数据,conn.Read 是一个阻塞函数
n, err := conn.Read(data)
if err != nil {
fmt.Println(err)
return
}
// 将字节串转字符串
fmt.Printf("接收到数据:%s\n", string(data[:n]))

// 4. 返回数据
_, err = conn.Write([]byte("Hello Client"))
if err != nil {
fmt.Println(err)
return
}
}

client/main.go,conn.Write 发送消息

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

import (
"fmt"
"net"
)

func main() {
// 1. 连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()

var word string
fmt.Print("请输入要发送的数据:")
fmt.Scanln(&word)

// 2. 发送数据,字符串转字节串
conn.Write([]byte(word))

// 3. 接收数据
data := make([]byte, 1024)
n, err := conn.Read(data)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("接收到数据:%s\n", string(data[:n]))
}

继续封装

服务端有两个阻塞,一个是连接阻塞,一个是 Read 阻塞。

server/main.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
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
package main

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

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

// 2. 服务端不断等待请求处理(注意 for 循环的位置,即便一个客户端断开后,也会等待其他客户端连接)
for {
// 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
continue
}
go ClientConn(conn)
}
}

// 3. 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 获取客户端地址
ipAddr := conn.RemoteAddr().String()
// 缓冲区
buf := make([]byte, 1024)
for {
// n 是读取的长度
// time.Sleep(time.Second*10) // 粘包
n, err := conn.Read(buf)
if err != nil {
fmt.Println(err)
return
}
// 切出有效数据
result := buf[:n]
// string(result) 将字节串转字符串
fmt.Printf("接收到数据,来自[%s] [%d]:%s\n", ipAddr, n, string(result))
// 接收到 exit,退出连接
if string(result) == "exit" {
fmt.Println(ipAddr, "退出连接")
return
}
// 4. 回复客户端
// []byte(xxx) 将字符串转字节串
conn.Write([]byte(strings.ToUpper(string(result))))
}
}

client/main.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
35
36
37
38
39
40
41
42
43
44
package main

import (
"fmt"
"net"
)

func main() {
// 1. 连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()

// 缓冲区
for {
/* var word string
fmt.Print("请输入要发送的数据:")
fmt.Scanln(&word)
conn.Write([]byte(word)) */

buf := make([]byte, 1024)
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
// string(buf) 将字节串转字符串
fmt.Printf("发送的内容:%s\n", string(buf))
// 2. 发送数据
conn.Write(buf)
// conn.Write(buf) // 粘包
// conn.Write(buf) // 粘包
// 接收服务端返回信息
res := make([]byte, 1024)
n, err := conn.Read(res)
if err != nil {
fmt.Println(err)
return
}
result := res[:n]
fmt.Printf("接收到数据:%s\n", string(result))
}
}

远程执行 SSH 命令

server/main.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
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
package main

import (
"fmt"
"net"
"os/exec"

"github.com/axgle/mahonia"
)

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

for true {
// 2. 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}

}

// 3. 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 获取客户端地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")
for true {
// 缓冲区
data := make([]byte, 1024)
// n 是读取的长度
n, err := conn.Read(data)
fmt.Println("命令字节数", n)
if err != nil {
fmt.Println(err)
return
}
// 4. 切出有效数据
data = data[:n]
fmt.Printf("接收到命令,来自[%s] [%d]:%s\n", ipAddr, n, string(data))
// 接收到 exit,退出连接
if string(data) == "exit" {
fmt.Println(ipAddr, "退出连接")
return
}
// 执行命令,string(data) => ls、touch
cmd := exec.Command("cmd", "/C", string(data))
// 5. 执行命令,并返回结果
output, err := cmd.Output()
if err != nil {
panic(err)
}
// 防止结果展示中文乱码
dec := mahonia.NewDecoder("gbk")
_, cdata, _ := dec.Translate(output, true)
result := string(cdata)
fmt.Println(result)
conn.Write([]byte(string(result)))
}
}

client/main.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
35
36
37
38
39
40
41
42
package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
)

func main() {
// 1. 连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
// 缓冲区
for true {

reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Println("输入执行命令>>>")
text, _ := reader.ReadString('\n') // 读到换行
text = strings.TrimSpace(text)

fmt.Println("text", text)
// 2. 发送数据
conn.Write([]byte(text))
// 3. 接收服务端返回信息
res := make([]byte, 100000) // 如何解决大文件传输问题呢?看下面的文件上传案例
n, err := conn.Read(res)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Println("n", n)
result := res[:n]
fmt.Printf("接收到数据:%s\n", string(result))
}
}

bash

1
touch index.html

最基础的 Web 服务

main.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
35
36
37
38
39
40
package main

import (
"fmt"
"net"
)

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

for {
// 2. 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}

}

// 3. 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 这里不必像之前那样再次加 for?因为浏览器输入地址每次敲回车都会重来上面的 for 一次
// 缓冲区
data := make([]byte, 1024)
// n 是读取的长度
n, _ := conn.Read(data)
fmt.Println(string(data[:n]))
conn.Write([]byte("HTTP/1.1 200 ok\r\n\r\nHello World"))
}

此时浏览器输入 http://127.0.0.1:8888/

🤠 如何返回图片?

1
conn.Write([]byte("HTTP/1.1 200 ok\r\n\r\n<h1>Hello World</h1><img src='http://gips3.baidu.com/it/u=3886271102,3123389489&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960'/>"))

把返回的内容抽离为单独的 index.html 文件。

1
2
<h1>Hello World</h1>
<img src="http://gips3.baidu.com/it/u=3886271102,3123389489&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960" />
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/ioutil"
"net"
)

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

for true {
// 2. 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}

}

// 3. 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 下面三行不能省略
data := make([]byte, 1024)
n, _ := conn.Read(data)
fmt.Println(string(data[:n]))
data, _ = os.ReadFile("index.html")
conn.Write([]byte("HTTP/1.1 200 ok\r\n\r\n" + string(data)))
}

HTTP 协议

请求协议

响应格式

演示 Content-Type

1
2
// ...
conn.Write([]byte("HTTP/1.1 200 ok\r\nContent-Type: text/plain\r\n\r\n" + string(data)))

根据 path 返回不同的资源,演示:

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"
"io/ioutil"
"net"
"strings"
)

func main() {
// 1. 创建 TCP 服务端监听
listenner, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println(err)
return
}
defer listenner.Close()

for true {
// 2. 阻塞等待客户端连接
conn, err := listenner.Accept()
if err != nil {
fmt.Println(err)
return
}
go ClientConn(conn)
}

}

// 3. 处理服务端逻辑
func ClientConn(conn net.Conn) {
defer conn.Close()
// 缓冲区
data := make([]byte, 1024)
// n 是读取的长度
n, _ := conn.Read(data)
requestStr := string(data[:n])
ret := strings.Split(requestStr, "\r\n")
path := strings.Split(ret[0], " ")[1]
if path == "/login" {
conn.Write([]byte("HTTP/1.1 200 ok\r\n\r\n" + "login ok"))
return
}
data, _ = ioutil.ReadFile("index.html")
// data, _ = os.ReadFile("index.html")
conn.Write([]byte("HTTP/1.1 200 ok\r\nContent-Type: text/plain\r\n\r\n" + string(data)))
}

Gin 框架基础

https://gin-gonic.com/zh-cn/

基础起步

初始化

1
2
3
go mod init quikestart

go get -u github.com/gin-gonic/gin // 或者先在 main.go 引入,然后执行 go mod tidy

http://localhost:8080

main.go

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

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
// 生成路由对象
router := gin.Default()

router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World")
})
router.GET("/login", func(c *gin.Context) {
c.String(http.StatusOK, "login ok")
})

router.Run(":8080")
}

返回 HTML 文件

router.LoadHTMLGlob("templates/*")c.HTML(http.StatusOK, "index.html", nil)

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

"github.com/gin-gonic/gin"
)

func home(c *gin.Context) {
// c.HTML(http.StatusOK, "index.html", gin.H{})
c.HTML(http.StatusOK, "index.html", nil)
}

func main() {
router := gin.Default()

// 指定模板目录
router.LoadHTMLGlob("templates/*")

router.GET("/", home)

router.Run(":8080")
}

templates/index.html

1
xx

返回 JSON 数据

c.JSON()

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

import (
"github.com/gin-gonic/gin"
)

func home(c *gin.Context) {
// type H map[string]interface{}
c.JSON(200, gin.H{
"message": "hello world",
"data": []string{"西游记", "国色天香"},
})
}

func main() {
router := gin.Default()

router.GET("/", home)

router.Run(":8080")
}

接受 POST 数据

登录案例:客户端编码 Content-Type: application/x-www-form-urlencoded,服务端 Gin 框架通过 c.PostForm() 函数接收数据。

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

import (
"github.com/gin-gonic/gin"
)

func login(c *gin.Context) {
c.HTML(200, "login.html", nil)
}

func auth(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
if username == "admin" && password == "123456" {
c.JSON(200, gin.H{
"message": "登录成功",
})
} else {
c.JSON(200, gin.H{
"message": "用户名或密码错误",
})
}
}

func main() {
router := gin.Default()

// 指定模板目录
router.LoadHTMLGlob("templates/*")

router.GET("/login", login)
router.POST("/auth", auth)

router.Run(":8080")
}

templates/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<form action="/auth" method="post">
<label for="username">用户名</label>
<input type="text" name="username" id="username" /><br />
<label for="password">密码:</label>
<input type="text" name="password" id="password" /><br />
<input type="submit" />
</form>
</body>
</html>

测试:http://127.0.0.1:8080/login

展示服务器时间

后端渲染模板和返回变量,c.HTML(200, "timer.html", gin.H{ "now": now, })

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

import (
"time"

"github.com/gin-gonic/gin"
)

func timer(c *gin.Context) {
now := time.Now().Unix()
c.HTML(200, "timer.html", gin.H{
"now": now,
})
}

func main() {
router := gin.Default()

// 指定模板目录
router.LoadHTMLGlob("templates/*")

router.GET("/timer", timer)

router.Run(":8080")
}

templates/timer.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>{{.now}}</h2>
</body>
</html>

路由定义

路由方法

RESTFul 风格路由

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

import (
"github.com/gin-gonic/gin"
)

func getUser(c *gin.Context) {
c.JSON(200, gin.H{
"message": "hello world",
})
}

func addUser(c *gin.Context) {
}

func getOneUser(c *gin.Context) {
}

func delOneUser(c *gin.Context) {
}

func putOneUser(c *gin.Context) {}

func main() {
router := gin.Default()

// 指定模板目录
router.LoadHTMLGlob("templates/*")

router.POST("/user", addUser)
router.DELETE("/user/:id", delOneUser)
router.PUT("/user/:id", putOneUser)
router.GET("/user", getUser)
router.GET("/user/:id", getOneUser)

// 无论什么请求,只要路径匹配就会命中
router.Any("/xxx", func(context *gin.Context) {

})

// 上面都匹配失败会走这儿
router.NoRoute(func(c *gin.Context) {})

router.Run(":8080")
}

路由分组

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 (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

bookRouter := router.Group("/book")
{
bookRouter.GET("/", func(context *gin.Context) {})
bookRouter.POST("/add", func(context *gin.Context) {})
}

publishRoute := router.Group("/publish")
{
publishRoute.GET("/", func(context *gin.Context) {})
publishRoute.POST("/add", func(context *gin.Context) {})
}

router.Run(":8080")
}

请求参数

获取请求信息

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

"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.GET("/test", func(c *gin.Context) {
// 1. 获取基本请求信息
fmt.Println(1, c.Request.Method) // GET
fmt.Println(2, c.Request.URL) // /test
fmt.Println(3, c.Request.RemoteAddr) // 127.0.0.1:58581
fmt.Println(4, c.ClientIP()) // 127.0.0.1

// 2. 获取请求头
fmt.Println(5, c.Request.Header) // map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[zh-CN,zh;q=0.9,en;q=0.8,zh-HK;q=0.7] Cache-Control:[max-age=0] Connection:[keep-alive] Cookie:[td_cookie=3045372747; BD_UPN=12314753; channel=127.0.0.1:5500; baikeVisitId=f8642c2a-d9c7-4aa6-a008-cc594c918429] Sec-Ch-Ua:["Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Windows"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[none] Sec-Fetch-User:[?1] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36]]
fmt.Println(6, c.Request.Header["User-Agent"]) // [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36]
fmt.Println(7, c.GetHeader("User-Agent")) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36

c.String(200, "test OK!")
})

router.Run(":8080")
}

获取路径参数

http://127.0.0.1:8080/user/123

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

import (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id")

c.String(200, id)
})

router.Run(":8080")
}

获取查询参数

http://127.0.0.1:8080/user?name=ifer

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 (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.GET("/user", func(c *gin.Context) {
// 1. c.Query
// name := c.Query("name")

// 2. c.DefaultQuery,可以通过第⼆个参数设置默认值
// name := c.DefaultQuery("name", "IFER")

// 3. 区别是 GetQuery 返回两个参数,第⼀个是参数值,第⼆个参数是参数是否存在的 bool 值,可以⽤来判断参数是否存在
// 参数为空,也算存在,不传才是不存在
name, ok := c.GetQuery("name")
if !ok {
c.String(400, "参数不存在")
return
}

c.String(200, name)
})

router.Run(":8080")
}

获取 PostForm

c.PostForm 能解析 x-www-form-urlencoded 和 form-data 类型的数据。

image-20250419093312359

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 (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.POST("/user", func(c *gin.Context) {
// 1. c.PostForm
// name := c.PostForm("name")

// 2. c.DefaultPostForm
// name := c.DefaultPostForm("name", "IFER")

// 3. c.GetPostForm
name, ok := c.GetPostForm("name")
if !ok {
c.JSON(400, gin.H{"error": "name is required"})
return
}
c.JSON(200, gin.H{"name": name})
})

router.Run(":8080")
}

PostFormArray

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

import (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.POST("/user", func(c *gin.Context) {
names := c.PostFormArray("names")
c.JSON(200, gin.H{"names": names})
})

router.Run(":8080")
}

关于 ShouldBind

基于请求的 Content-Type 自动提取请求中 querystring、form 表单、JSON、XML 等参数到结构体中,⽀持 Get/Post 请求。

还有其他特定的方法,例如 c.ShouldBindJSON()、c.ShouldBindXML()、c.ShouldBindQuery()、c.ShouldBindYAML() 等。

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

"github.com/gin-gonic/gin"
)

type User struct {
Name string `json:"name" form:"name"`
Age int `json:"age" form:"age"`
Email string `json:"email" form:"email"`
}

func main() {
r := gin.Default()

r.POST("/user", func(c *gin.Context) {
// 1. 初始化 User 结构体
user := User{}

// 2. 通过 ShouldBind 函数,将请求参数绑定到 struct 对象
if c.ShouldBind(&user) == nil {
log.Println("user:", user)
}

// 3. 返回响应
c.JSON(200, gin.H{
"msg": "Parser Success",
"request content-type": c.ContentType(),
"data": user,
})
})

r.Run(":8080")
}

响应数据

不同格式

字符串

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

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/user", func(c *gin.Context) {
c.String(200, "Hello World")
})

r.Run(":8080")
}

HTML

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

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.LoadHTMLGlob("templates/*")

r.GET("/user", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})

r.Run(":8080")
}

JSON

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

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello, user!",
})
})

r.Run(":8080")
}

XML

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

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/user", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{
"message": "Hello, XML!",
})
})

r.Run(":8080")
}

静态文件

http://127.0.0.1:8080/static/img.png

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

import (
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.Static("/static", "./static")

r.Run(":8080")
}

重定向

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

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

r.GET("/user", func(c *gin.Context) {
// c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
// 相当于重新发了请求 /index
c.Redirect(http.StatusMovedPermanently, "/index")
})

r.GET("/index", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})

r.Run(":8080")
}

模板定义

渲染取值

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 "github.com/gin-gonic/gin"

type Student struct {
Name string
Age int
}

func main() {
r := gin.Default()
// 1. 加载模板文件
r.LoadHTMLGlob("templates/*")

r.GET("/index", func(context *gin.Context) {
// 2. 渲染模板
context.HTML(200, "index.html", gin.H{
// 字符串
"user": "IFER",
// 切片
"addressSlice": []string{"上海", "深圳", "武汉"},
// Map
"stuMap": map[string]interface{}{
"name": "ELSER",
"age": 18,
"hobby": []string{"吃饭", "睡觉", "打豆豆"},
},
// 结构体
"stuStruct": Student{Name: "IFER", Age: 22},
// 切片套结构体
"stuSlice": []Student{{Name: "IFER", Age: 22}, {Name: "ELSER", Age: 20}},
})
})

// 3. 默认 8080
r.Run()
}

index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<h3>变量渲染</h3>
<!-- 所有的 -->
{{ . }}
<!-- 切片 -->
<p>{{.addressSlice}}</p>
<!-- index 表示索引操作,获取切片中的某一个 -->
<p>{{index .addressSlice 0 }}</p>
<p>{{index .addressSlice 1}}</p>
<p>{{index .addressSlice 2}}</p>
<!-- Map -->
<p>{{.stuMap}}</p>
<p>{{.stuMap.name}}</p>
<!-- 结构体 -->
<p>{{.stuStruct}}</p>
<p>{{.stuStruct.Name}}</p>
<!-- 切片套结构体,先变量赋值,再使用变量 -->
<p>{{$s := index .stuSlice 0}}</p>
<p>{{$s.Name}}</p>
</body>
</html>

条件循环

条件判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
{{ if gt .stuStruct.Age 18 }}
<h1>成年</h1>
{{ else }}
<h2>未成年</h2>
{{ end }}
</body>
</html>

循环语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<!-- 循环结构体的切片 -->
{{ range $index, $stu := .stuSlice }}
<p>{{$index}} - {{$stu.Name}}</p>
{{ end }}
</body>
</html>

其他操作

模板函数、嵌套和继承 …

学习 GORM

https://gorm.io/

https://www.kancloud.cn/sliver_horn/gorm

1
2
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

连接数据库

基础语法

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

import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name
dsn := "username:password@tcp(127.0.0.1:3306)/database?charset=utf8mb4&parseTime=True&loc=Local"
// mark
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
fmt.Println(db, err)
}

配置日志

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

import (
"fmt"
"log"
"os"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/go?charset=utf8mb4&parseTime=True&loc=Local"
// 1. 创建日志对象
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
// SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
},
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 2. 日志配置
Logger: newLogger,
})

if err != nil {
panic("failed to connect database")
}
fmt.Println(db)
}

关于表操作

模型声明

🤔 选课系统。

学生(多):班级(一)、学生(多):课程(多)

老师(一):班级(多)、老师(一):课程(多)

班级

课程

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 (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)

type BaseModel struct {
ID int `gorm:"primaryKey"`
CreateTime *time.Time `gorm:"autoCreateTime"`
UpdateTime *time.Time `gorm:"autoCreateTime"`
}

type Teacher struct {
BaseModel
Name string `gorm:"type:varchar(32);unique;not null"`
Tno int
Pwd string `gorm:"type:varchar(100);not null"`
Tel string `gorm:"type:char(11);"`
Birth *time.Time
Remark string `gorm:"type:varchar(255);"`
}


// ...

模型迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
"log"
"os"
"time"

"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

type BaseModel struct {
ID int `gorm:"primaryKey"`
CreateTime *time.Time `gorm:"autoCreateTime"`
UpdateTime *time.Time `gorm:"autoCreateTime"`
}

type Teacher struct {
BaseModel
// gorm.Model // 建议用官方自带的,里面包含 ID、CreatedAt、UpdatedAt、DeletedAt 四个字段
Name string `gorm:"type:varchar(32);unique;not null"`
Tno int
Pwd string `gorm:"type:varchar(100);not null"`
Tel string `gorm:"type:char(11);"`
Birth *time.Time
Remark string `gorm:"type:varchar(255);"`
}

func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/go?charset=utf8mb4&parseTime=True&loc=Local"

// 创建日志对象
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
// SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
},
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger, // 日志配置
})
if err != nil {
panic("failed to connect database")
}

db.AutoMigrate(&Teacher{})
fmt.Println(db)
}

运行

1
go run main.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
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
package main

import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

type BaseModel struct {
ID int `gorm:"primaryKey"`
CreateTime *time.Time `gorm:"autoCreateTime"`
UpdateTime *time.Time `gorm:"autoCreateTime"`
}

type Teacher struct {
BaseModel
Name string `gorm:"type:varchar(32);unique;not null"`
Tno int
Pwd string `gorm:"type:varchar(100);not null"`
Tel string `gorm:"type:char(11);"`
Birth *time.Time
Remark string `gorm:"type:varchar(255);"`
}

type Class struct {
BaseModel
Name string `gorm:"type:varchar(32);unique;not null"`
Num int
// 1. Class 和 Teacher 之间是多对一的关系
TeacherID int
Teacher Teacher
}

type Course struct {
BaseModel
Name string `gorm:"type:varchar(32);unique;not null"`
Credit int // 学分
Period int // 学时
// 2. Course 和 Teacher 之间是多对一的关系
TeacherID int
Teacher Teacher
}

type Student struct {
BaseModel
Sno int
Name string `gorm:"type:varchar(32);not null"`
Pwd string `gorm:"type:varchar(100);not null"`
Tel string `gorm:"type:char(11);"`
Gender byte `gorm:"default:1"`
Birth *time.Time
Remark string `gorm:"type:varchar(255);"`

// 3. Student 和 Class 之间是多对一的关系
ClassID int
Class Class `gorm:"foreignKey:ClassID"`
// Student 和 Courses 之间是多对多的关系
Courses []Course `gorm:"many2many:student2course;constraint:OnDelete:CASCADE;"`
}

func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/go?charset=utf8mb4&parseTime=True&loc=Local"

// 创建日志对象
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
// SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
},
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger, // 日志配置
})
if err != nil {
panic("failed to connect database")
}

db.AutoMigrate(&Teacher{})
db.AutoMigrate(&Course{})
db.AutoMigrate(&Class{})
db.AutoMigrate(&Student{})
fmt.Println(db)
}

表记录添加

添加记录

db.Create()

1
2
3
4
5
t1 := Teacher{Name: "IFER", Tno: 1001, Pwd: "123"}
// 要传递指针,这儿还有个回写的功能
db.Create(&t1)
// 看这儿,多了个 ID 字段
fmt.Println(t1.ID)

批量添加多对一关联表的记录

班级

1
2
3
4
5
6
7
8
9
10
class01 := Class{Name: "软件1班", TeacherID: 1}
class02 := Class{Name: "软件2班", TeacherID: 1}
class03 := Class{Name: "计算机科学与技术1班", TeacherID: 2}
class04 := Class{Name: "计算机科学与技术2班", TeacherID: 2}
classes := []Class{class01, class02, class03, class04}
db.Create(&classes)

for _, class := range classes {
fmt.Println(class.ID) // 1,2,3
}

课程

1
2
3
4
5
6
7
course01 := Course{Name: "计算机网络", Credit: 3, Period: 16, TeacherID: 1}
course02 := Course{Name: "数据结构", Credit: 2, Period: 24, TeacherID: 1}
course03 := Course{Name: "数据库", Credit: 2, Period: 16, TeacherID: 2}
course04 := Course{Name: "数字电路", Credit: 3, Period: 12, TeacherID: 2}
course05 := Course{Name: "模拟电路", Credit: 1, Period: 8, TeacherID: 2}
courses := []Course{course01, course02, course03, course04, course05}
db.Create(&courses)

添加多对多关联表的记录

两种方式:创建学生的同时绑定多对多、查询学生绑定多对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
var courses []Course
db.Where("name in ?", []string{"数据结构", "数据库"}).Find(&courses)

s1 := Student{
Name: "张三",
Pwd: "123456",
ClassID: 1,
Courses: courses,
}
db.Create(&s1)

fmt.Println(s1.Class.Name)
1
2
3
4
5
6
7
8
9
10
11
var courses []Course
db.Where("name in ?", []string{"计算机网络", "数字电路与逻辑设计"}).Find(&courses)

s1 := Student{
Name: "李四",
Pwd: "123456",
ClassID: 1,
}
db.Create(&s1)

db.Model(&s1).Association("Courses").Append(courses)

查询学生绑定多对多关系。

1
2
3
4
5
6
var courses []Course
db.Where("name in ?", []string{"计算机网络", "数字电路与逻辑设计"}).Find(&courses)

var student = Student{}
db.Where("name = ?", "赵六").First(&student)
db.Model(&student).Association("Courses").Append(courses)

解除多对多的关系。

1
2
3
4
5
6
var courses []Course
db.Where("name in ?", []string{"计算机网络", "数字电路与逻辑设计"}).Find(&courses)

var student = Student{}
db.Where("name = ?", "赵六").First(&student)
db.Model(&student).Association("Courses").Delete(courses)

查询张三选修了哪些课程。

1
2
3
4
5
6
7
8
var student Student
db.Where("name = ?", "张三").First(&student)
var courses []Course
db.Model(&student).Association("Courses").Find(&courses)

for _, course := range courses {
fmt.Printf("- %s (学分: %d, 学时: %d)\n", course.Name, course.Credit, course.Period)
}

表记录查询

查询全部记录

1
2
3
4
5
6
7
8
9
// 1. 首先声明一个切片
// var teachers []Teacher
teachers := []Teacher{}
// 2. 执行 db.Find() 方法
db.Find(&teachers)
// 3. 遍历切片
for _, teacher := range teachers {
fmt.Printf("教师ID: %d, 教师姓名: %s\n", teacher.ID, teacher.Name)
}

查询单条记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 准备一个结构体实例
course := Course{}
// 2. 执行查询操作
// SELECT * FROM courses LIMIT 1;
// db.Take(&course)

// SELECT * FROM `courses` ORDER BY `id` LIMIT 1;
// db.First(&course)

// SELECT * FROM `courses` ORDER BY `id` DESC LIMIT 1;
// db.Last(&course)

// SELECT * FROM `courses` WHERE credit < 3 ORDER BY credit asc LIMIT 1;
// 查询学分小于 3 的课程,并按学分升序排序
db.Where("credit < ?", 3).Order("credit asc").Take(&course)
fmt.Println(course)

Where 查询

1
2
3
course := []Course{}
db.Where("credit > ?", 3).Find(&course)
fmt.Println(course)
1
2
3
var total int64
db.Model(&Course{}).Where("credit between ? and ?", 1, 3).Count(&total)
fmt.Println(total)

表记录删除

1
2
3
var course Course
db.Where("name = ?", "模拟电路").Take(&course)
db.Delete(&course)

表记录更新

1
2
3
4
var course Course
db.Where("name = ?", "数字电路").Find(&course)
course.Name = "数字电路与逻辑设计"
db.Save(&course)
1
2
// 所有的课程学分都改为 3
db.Model(&Course{}).Where("1=1").Update("Credit", 3)

关联表查询

查询软件 1 班班主任的名字。

1
2
3
var class Class
db.Preload("Teacher").Where("name = ?", "软件1班").Find(&class)
fmt.Println(class.Teacher.Name)

查询赵六所在班级的人数。

1
2
3
var student Student
db.Preload("Class").Where("name = ?", "赵六").Find(&student)
fmt.Println(student.Class.Num)

查询张三的班级的名称和选修课程的名字及课程学分。

1
2
3
4
5
var student Student
db.Preload("Class").Preload("Courses").Where("name = ?", "张三").Find(&student)
for _, course := range student.Courses {
fmt.Println(course.Name, course.Credit)
}

嵌套预加载:查询张三所在班级的班主任(老师)的名字。

1
2
3
var student Student
db.Preload("Class").Preload("Class.Teacher").Where("name = ?", "张三").Find(&student)
fmt.Println(student.Class.Teacher.Name)

反向查询:软件 2 班有哪些学生。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 完善 Class 结构体
/* type Class struct {
// ...
// 无需执行迁移操作
Students []Student
} */

// 2. 查询
var class Class
db.Preload("Students").Where("name = ?", "软件1班").Find(&class)
for _, student := range class.Students {
fmt.Println(student.Name)
}

一些代码备份

错误处理

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
// 自定义错误类型
type MyError struct {
Code int
Message string
}

// 实现 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("错误码:%d,错误信息:%s", e.Code, e.Message)
}

// 使用自定义错误
func doSomething() error {
return &MyError{
Code: 404,
Message: "资源未找到",
}
}

func main() {
if err := doSomething(); err != nil {
fmt.Println(err) // 输出: 错误码:404,错误信息:资源未找到

// 类型断言获取详细错误信息
// *MyError 表示 指向 MyError 结构体的指针类型
// 第一个值 myErr:如果断言成功,它就是 err 实际存储的 *MyError 类型的值
// ok 为 true 表示断言成功,false 表示断言失败
if myErr, ok := err.(*MyError); ok {
fmt.Printf("错误码:%d\n", myErr.Code)
}
}
}

make 函数

如果只声明指针、切片、map、通道,那么默认都是 nil,不能直接操作,所以需要使用 make 来初始化或字面量初始化,如 s = make([]int, 5)s := []int{1, 2, 3},映射声明后也为 nil,需要使用 make 函数或映射字面量初始化才能使用,如 var m map[string]int; m = make(map[string]int)m := map[string]int{"key": "value"}。通道同样需要使用 make 函数初始化,如 var ch chan int; ch = make(chan int)

指针接收者与值接收者

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

// 值接收者 - 不会修改原始值
func (c Counter) Increment() Counter {
return c + 1
}

// 指针接收者 - 会修改原始值
func (c *Counter) Add(val int) {
*c += Counter(val)
}

func main() {
var c Counter = 5

// 值接收者不会修改c
fmt.Println(c.Increment()) // 输出: 6
fmt.Println(c) // 输出: 5

// 指针接收者会修改c
c.Add(10) // 自动解引用
fmt.Println(c) // 输出: 15
}