照烟照烟
笔记

Go

概述

一门后端语言可以分成数据处理+外部交互+类型抽象+并发编排四部分。

基础配置

选择一个空白文件夹,在终端中执行

1
go mod init xxx

其中 xxx 是项目名。若要推送到 github,尽量规范命名:github.com/cheyanne/project

项目根目录创建 main.go,其中开头必须是 package main ,用于标记程序入口。想要自己写包,在项目文件夹下创建包名文件夹,包名文件夹下创建同名的 go 文件,开头写 package [包名]公开的函数以大写字母开头,内部函数以小写字母开头。

最终导入包时,按照

1
2
3
4
import(
“[项目名]/[包名]”
...
)

的格式导入。

运行:go run .

编译:go build -o xxx.exe(Win 平台下)

语法

Go 命名规范:

函数、变量、结构体(包括这其中的变量、函数 / 方法)等

首字母大写时外部包可访问。小写时无法访问到。

接收器命名:

结构体方法的接收器用结构体首字母小写,简短统一。

struct

普通结构体:

1
2
3
4
5
type Addr struct {
Province string
City string
District string
}

嵌套结构体:

1
2
3
4
5
6
type UserInfo struct {
Name string
Age int
Sex string
Addr Addr
}

Go 允许自定义变量类型和内部变量名同名。上面的也可以写成匿名形式,不过可读性比较差:

1
2
3
4
5
6
7
8
type UserInfo struct {
Name string
Age int
Sex string
Addr
}
// 这么写可以直接访问user.City 而不用再套一层user.Addr.City
// 好像没必要

实例化对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var user = UserInfo{
Name: "Cheyanne",
Age: 18,
Sex: "female",
Addr: Addr{
Province: "Henan",
City: "Zhengzhou",
District: "Erqiqu",
},
}
// 嵌套结构体时 要写明内部结构体的类型
// map和struct都是这样
// struct内部直接是可直接使用的类型变量
// map内部的键值元素类型需要自定义

u := User{
Name: "Alice",
Age: 28,
Email: "",
Password: "secret_password",
Roles: []string{"admin", "editor"},
}

结构体函数:

1
2
3
4
5
6
7
func (u *UserInfo) SetAge(newAge int) {
u.Age = newAge
}

func (u *UserInfo) SetProvince(newProvince string) {
u.Addr.Province = newProvince
}

前面的括号指明这是一个结构体函数,传入指针可以直接修改信息。

序列化:

1
2
3
4
5
6
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Password string `json:"-"`
Tags []string `json:"tags,omitempty"`
}

结构体变量后面反引号包裹的内容相当于机器注释。在这里,json:"name" 表示导入后的键名为 name,而非 Namejson:"-" 表示忽略该项。

底层库在读取这段文本时,会按照 json:"[键名],[备注1],[备注2]" 的顺序读取。即识别到的第一个为键名。omitempty 是 omit it if empty 的意思,即若为空则忽略该项。

对于这个 User 结构体,序列化函数:

1
2
3
4
5
func (u *User) Serialize() string {
jsonData, _ := json.Marshal(u)
// 注意将[]byte转为string
return string(jsonData)
}

反序列化:

1
2
3
4
5
func UnSerialize(jsontxt string) User {
var return_user User
json.Unmarshal([]byte(jsontxt), &return_user)
return return_user
}

map

创建一个空 map,指明键值对类型:

1
NewMap := make(map[any]any)

也可以初始化时赋值:

1
2
3
4
5
6
7
8
9
10
NewMap := map[string]any{
"name": "XiaoMing",
"age": 18,
"sex": "male",
"hobbies": []string{
"cooking",
"play games",
},
}
// 键不像结构体var,没有大小写访问规则。只要map可访问,键值对就可访问

访问:

1
2
3
val, ok := NewMap["sex"].(string)
// 此时sex是any类型 需要显式声明为string
hbs, ok := NewMap["hobbies"].([]string)

修改 / 添加键值对:

1
NewMap["sex"] = "female"

删除键值对:

1
delete(NewMap, "age")

打印值(一定使用 fmt.Print,不要使用内置的 print):

1
fmt.Println(NewMap["hobbies"])

序列化:

对于需要在内存中操作的场景,使用json.Marshal

1
2
3
4
5
6
newMap := map[string]string{
"name": "xiaoming",
"sex": "male",
}
raw_data, _ := json.Marshal(newMap)
data := string(raw_data)

直接流式输出(控制台 / http):

1
2
3
4
5
6
7
8
9
10
11
12
13
newMap := map[string]string{
"name": "xiaoming",
"sex": "male",
}

// 终端输出
json.NewEncoder(os.Stdout).Encode(newMap)

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// http输出
// 把ResponseWriter作为流式输出端口
json.NewEncoder(w).Encode(newMap)
})

slice

创建某类型的切片:

1
2
3
4
s := make([]string, 0)
var s []string
// 可指定长度和容量,至少指定容量
// 不知道多少就传个0进去,后期可以用append添加

带有数据的初始化:

1
list := []string{"apple", "banana"}

增删改查:

1
2
3
4
5
6
7
8
list = append(list, "grape") // 增加
list[0] = "orange" // 改
first := list[0] // 访问

var delIndex = 1
sli = append(sli[:delIndex], sli[delIndex+1:]...)
// 注意这里的三个点 表示把sli[delIndex+1:]中含有的元素解包
fmt.Println(sli)

遍历:

1
2
3
4
5
6
for _, value := range sli {
fmt.Println(value)
}
// range slice本质是一个表达式,依次取出(index, slice[index])
// 使用index, val接收返回值
// enumerate

func

函数格式:

1
2
3
4
func function(param1 any, param2 any) any {
xxx
return xxx
}

结构体函数:

1
2
3
4
5
6
// 传指针,可以修改变量的结构体元素
func (u *Userinfo) function(param1 any, param2 any) any {
xxx
// 内部可访问结构体元素
return xxx
}

Go 不允许嵌套函数,但是可以定义匿名函数赋值给变量。

匿名函数:

1
2
3
add := func(a int, b int) int {
return a + b
}

闭包:

针对同一函数内,如果定义匿名函数并复制给变量,则匿名函数的作用域是整个函数周期,且能够有效操作内部变量。

1
2
3
4
x := 10
f := func(){
x++
}

例如删除切片元素的函数:

1
2
3
4
5
6
func DelSliceElem(delIndex int, slice []any) []any {
if delIndex < 0 || delIndex >= len(slice) {
return slice
}
return append(slice[:delIndex], slice[delIndex+1:]...)
}

默认返回:

当在函数签名里额外声明了返回值的变量名(而不仅仅是类型),就可只写 return 语句,此时函数会自动把相应变量带出去。

当函数内部有 defer 语句时,无法直接在 defer 内让原函数显式返回。这时候就需要在签名时声明返回值的变量名,静静等待函数把相应的变量带出。

错误处理 + (defer + recover)

在函数返回值中可以显式返回一个 error 型变量。当能够预知到传参或运行途中可能会出现某些状况导致 panic,可以提前处理边界情况。

1
2
3
4
5
6
7
func safeDivide(a, b int) (res float64, err error) {
if b == 0 {
err = fmt.Errorf("不能除以0!")
return 0, err
}
return float64(a) / float64(b), nil
}

字符串可以通过 fmt.Errorf 被复制到 error,进而被回显。

主函数的错误处理:

1
2
3
4
5
6
7
8
9
func main() {
res, err := safeDivide(1, 0)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("res: %f\n", res)
}
fmt.Println("done")
}

使用 defer+recover 结构可以让程序在出错时不至于直接退出,能够继续接下来的业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func safeDivide(a, b int) (res int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("自定义错误类型(%v)", r)
}
}()
return a / b, nil
}

func main() {
res, err := safeDivide(1, 0)
if err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%d\n", res)
}
fmt.Println("done")
}

在这段程序中,使用了命名返回值。如果 b 不为 0,则 a/b 没有问题,正常返回 res, nil。当 b 为 0 时,程序计算到 a/b 时会崩溃,进而抛出 panic。由于写了 defer 结构,这个 panic 会被 recover 捕获到(并消解 panic 的强制退出性质),并被赋值给 r。此时走 defer 函数的 if 分支:err 被格式化的字符串赋值,最终程序把没有赋值的 res(默认为 0)和自定义的 err 带出。

当 defer 结构是匿名函数时,需要在函数定义的末尾写一对小括号表示立即调用,否则只会定义这个函数,defer 实际为空。

Functional Options

FO 解决了函数传参时需要按固定顺序、形参没有默认值的难点。

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 makeburger

type burger struct {
bun bool
beef_patty bool
cheese bool
sauce bool
pickles bool
}

type option func(*burger)

func WithCheese(cheese bool) option {
return func(b *burger) {
b.cheese = cheese
}
}

func WithSauce(sauce bool) option {
return func(b *burger) {
b.sauce = sauce
}
}

func WithPickles(pickles bool) option {
return func(b *burger) {
b.pickles = pickles
}
}

func NewBurger(opts ...option) *burger {
b := &burger{
bun: true,
beef_patty: true,
cheese: false,
sauce: false,
pickles: false,
}

for _, opt := range opts {
opt(b)
}

return b
}

type option func(*burger) 一句定义了一个匿名函数 option。传入结构体指针可以直接修改作用域外的结构体变量的值,也无需返回值。更重要的是创建 option 的意义。在通常情况下,只有实例化一个结构体之后,才能对这个对象进行修改。这里把函数模板提前定义出来,是为了满足后续 With 函数对此模板函数(option)的修改需求(初始化一个注射器)

后面的自定义函数 With... 接收布尔值,返回 option。布尔值是用户想要对实例化对象执行的操作。假如用户对某项的需求为 true,则直接扔进去,得到一个自定义的 option(这个 option 被调用后 能够使对象的对应值改变,相当于给注射器灌药)

最后的实例化函数接收多个 options(花式注射器),返回一个被修改过的结构体实例的指针。其中 (opts ...options) 把传入的所有 option 打包为切片,便于访问。在实例化函数中,先创建一个 默认参数的 结构体,并将其指针赋值给内部变量。接着遍历切片,依次把每个 option 作用于对象(因为原 option 模板规定传入指针所以可以修改,相当于把所有注射器扎到对象上),最终返回这个对象(所有更改已经正式生效)的指针。

os (file IO)

系统级的文件 IO 操作分为两大类:

  1. 对某一具体文件进行修改(按文件大小分为大文件和小文件,二者要用不同的方法处理);追加文件内容
  2. 管理目录结构:
    1. 创建目录(文件、单层文件夹、嵌套创建多层文件夹)
    2. 删除(文件、文件夹)
    3. 遍历目录(读取文件夹及其文件、递归读取、在多文件中查找内容)
    4. 改名
    5. 移动
    6. 获取元信息
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
package singlefile

import (
"io"
"os"
"path/filepath"
)

var MainPath = filepath.Join(".", "test.txt")

// 测试文件路径是否可用
func TestPath(ph string) error {
_, err := os.Stat(ph)

return err
}

// 加载小文件内的所有内容
func LoadContent(ph string) (string, error) {
file, err := os.OpenFile(ph, os.O_RDONLY, 0644)
if err != nil {
return "", err
}
defer file.Close()

raw_data, err := io.ReadAll(file)
if err != nil {
return "", err
}

return string(raw_data), nil
}

// 上一函数更简单的实现
func LoadContent_simple(ph string) (string, error) {
raw_data, err := os.ReadFile(ph)
if err != nil {
return "", err
}

return string(raw_data), nil
}

// 重写整个文件
func RewriteFile(ph, content string) error {
file, err := os.OpenFile(ph, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(content)
if err != nil {
return err
}

return nil
}

// 更简单的实现
func RewriteFile_simple(ph, content string) error {
return os.WriteFile(ph, []byte(content), 0644)
}

// 读取文件某行的指定内容
func ReadLine(ph string, targetLineNum int) (string, error) {
file, err := os.OpenFile(ph, os.O_RDONLY, 0644)
if err != nil {
return "", err
}
defer file.Close()

scanner := bufio.NewScanner(file)

lineNum := 1
for scanner.Scan() {
if lineNum == targetLineNum {
return scanner.Text(), nil
}
lineNum++
}

if err := scanner.Err(); err != nil {
return "", err
}
return "", fmt.Errorf("line %d not found", targetLineNum)
}

// 在文件末尾追加内容
func AppendContent(ph, content string, isNewline bool) error {
file, err := os.OpenFile(ph, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer file.Close()

if isNewline {
_, err := file.WriteString("\n" + content)
return err
} else {
_, err := file.WriteString(content)
return err
}
}

Gin框架