结构体

在 Go 语言中,结构体(struct)是一种复合数据类型,用于将多个不同类型的数据组合在一起,类似于其他语言中的类(class)对象(object)的雏形。

基础定义,这个 Person 结构体包含两个字段:Name(字符串类型)和 Age(整数类型)。

1
2
3
4
type Person struct {
Name string
Age int
}

创建结构体对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var p1 Person
p1.Name = "张三"
p1.Age = 18
fmt.Println(p1)

// 另一种写法:使用字面量初始化
p2 := Person{Name: "李四", Age: 20}
fmt.Println(p2)

// 使用指针(推荐做法)
p3 := &Person{Name: "王五", Age: 22}
fmt.Println(p3.Name)
}

虽然 Go 没有“类”,但可以给结构体绑定方法,写法func (p Person) SayHi() {},p是接收者,类似python的self,表示调用者本身,接收者可以是值也可以是指针。

1
2
3
4
5
6
7
8
func (p Person) SayHi() {
fmt.Printf("你好,我叫 %s,今年 %d 岁\n", p.Name, p.Age)
}
// 调用
p1 := Person{Name: "小明", Age: 30}
p1.SayHi()
p3 := &Person{Name: "王五", Age: 22}
p3.SayHi()

在Go中没有继承,但可以使用结构体嵌套的方式实现类似继承的效果,就是B结构体包含A结构体的属性和方法

匿名嵌套: 使用时可以直接u.City,只要City不冲突,如果冲突了必须使用u.Address.City调用

显式嵌套: 如果写成 Addr Address,则必须通过 user.Addr.City 访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Address struct {
City string
ZipCode string
}

type User struct {
Name string
Age int
Address // 匿名嵌套(嵌入)
}

func (u User) GetInfo() {
fmt.Printf("姓名:%s 年龄:%d 地址:%s-%s", u.Name, u.Age, u.City, u.ZipCode)
}

func main() {
a1 := Address{City: "北京", ZipCode: "100000"}
u1 := &User{Name: "张三", Age: 18, Address: a1}
u1.GetInfo()
}

结构体Tag

Go 的结构体 tag(标签) 是结构体字段后面的一段字符串信息,主要用于告诉一些库(如 JSON 编解码、数据库 ORM、表单绑定等)如何处理这个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Address struct {
City string `json:"city"`
ZipCode string `json:"zip_code"`
}

type User struct {
Name string `json:"name"`
Age int `json:"age"`
Address // 匿名嵌套(嵌入)
}

func main() {
a1 := Address{City: "北京", ZipCode: "100000"}
u1 := &User{"张三", 12, a1}
bytesData, _ := json.Marshal(u1)
fmt.Println(string(bytesData))
}
// 输出 {"name":"张三","age":12,"city":"北京","zip_code":"100000"}

使用-可以隐藏一个字段,比如password字段不想返回给前端

使用omitempty可以忽略空值, 比如:json:"age,omitempty"

自定义数据类型

Go 的自定义数据类型,是指使用 type 关键字给现有类型 起一个新名字,或者定义一个全新的结构体、接口等类型

1
2
type MyInt int // 自定义类型,虽然底层是int类型,但MyInt是独立的类型
type MyString = string // 类型别名,和string完全一样,不能添加新方法

自定义类型有什么用?目前看着好像没啥用哦,有的兄弟,肯定有的;可以给原始类型添加新方法,比如求一个整数的奇偶

1
2
3
4
5
6
7
8
type MyInt int
func (m MyInt) odd() bool {
return m%2 == 1
}
func main() {
var a1 MyInt = 2
fmt.Println(a1.odd())
}

接口

Go 中的接口(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
type Speaker interface {
Speak()
}

type People struct{}

type Basketball struct {
}

func (p People) Speak() {
fmt.Println("别狗叫")
}

func (b Basketball) Speak() {
fmt.Println("大爷,来玩呀")
}

func identify(s Speaker) {
s.Speak()
}

func main() {
p := People{}
b := Basketball{}
identify(p) // 输出:别狗叫
identify(b) // 输出:大爷,来玩呀
}

类型断言

在Go语言中,类型断言用于从一个接口类型的变量中获取它的具体类型的值。

简单写法,判断是否为某一种类型:sType, ok := s.(People)

switch写法,返回它属于那种类型

1
2
3
4
5
6
7
8
9
10
11
func identify(s Speaker) {
switch sType := s.(type) {
case People:
fmt.Println("狗叫?", sType)
case Basketball:
fmt.Println("ik", sType)
default:
fmt.Println("未知类型", sType)
}
s.Speak()
}

空接口

Go 的 空接口(interface{} 是 Go 语言中非常重要、也非常常用的一个特性。它是一个没有任何方法的接口,表示任意类型,any就是一个空接口的别名。

协程(重点:高中)

Go 的协程叫 goroutine,是 Go 语言并发编程的核心,轻量、高效。goroutine 是 Go 语言运行时管理的一个轻量级线程。

可以理解成一个执行函数的“独立小线程”,但它比传统的系统线程更轻,启动更快、占用资源更少。

这是一个同步函数,按照顺序“狗叫”,为了让他们三同时“狗叫”,所以使用协程,让他们到三个小房间同时狗叫。

1
2
3
4
5
6
7
8
9
10
11
12
13
func dogBarking(name string) {
fmt.Println(name, "开始狗叫")
time.Sleep(1 * time.Second)
fmt.Println(name, "狗叫结束")
}

func main() {
startTime := time.Now()
dogBarking("张三")
dogBarking("李四")
dogBarking("王五")
fmt.Println("狗叫完成", time.Since(startTime))
}

在函数前加上go就可以同时运行,但是输出是: “狗叫完成 0s”, 因为主线程没有等待他们“狗叫完成”就结束了。

1
2
3
4
5
6
7
8
func main() {
startTime := time.Now()
go dogBarking("张三")
go dogBarking("李四")
go dogBarking("王五")
fmt.Println("狗叫完成", time.Since(startTime))
}
// 输出 狗叫完成 0s

使用sync.WaitGroup来等待异步函数结束

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

var wait sync.WaitGroup

func dogBarking(name string) {
defer wait.Done()
fmt.Println(name, "开始狗叫")
time.Sleep(1 * time.Second)
fmt.Println(name, "狗叫结束")
}

func main() {
startTime := time.Now()
wait.Add(3)
go dogBarking("张三")
go dogBarking("李四")
go dogBarking("王五")
wait.Wait()
fmt.Println("狗叫完成", time.Since(startTime))
}

通道(channel) (重点:高中)

在 Go 中,channel(通道) 是一种 用于 goroutine 之间通信的机制,你可以把它理解为一个“消息管道”或者“线程安全的队列”。

通俗易懂理解:goroutine 是单个人在干活,channel 是人和人之间的“对讲机”或“传送带”。

使用通道来接收异步函数返回的结果, 类似python的await关键字

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

func dogBarking(name string, ch chan string) {
fmt.Println(name, "开始狗叫")
time.Sleep(1 * time.Second)
fmt.Println(name, "狗叫结束")
ch <- fmt.Sprintf("%s 狗叫完了", name)
}

func main() {
startTime := time.Now()
ch := make(chan string)
go dogBarking("张三", ch)
go dogBarking("李四", ch)
go dogBarking("王五", ch)
// 用 channel 接收3个结果,阻塞等待 goroutine 完成
for i := 1; i <= 3; i++ {
msg := <-ch
fmt.Println("收到消息:", msg)
}
fmt.Println("狗叫完成", time.Since(startTime))
}

// 输出
//王五 开始狗叫
//李四 开始狗叫
//张三 开始狗叫
//张三 狗叫结束
//王五 狗叫结束
//李四 狗叫结束
//收到消息: 张三 狗叫完了
//收到消息: 王五 狗叫完了
//收到消息: 李四 狗叫完了
//狗叫完成 1.0108759s

select结构和超时

select 是 Go 里用来同时监听多个 channel 操作的控制结构。它类似于 switch,但是专门处理 channel 的发送和接收。

select 的作用

  • 等待多个 channel 的操作(发送或接收)其中一个准备好
  • 一旦某个 channel 操作就绪,就执行对应的 case
  • 如果多个 channel 同时就绪,会随机选一个执行
  • 可以用来实现超时、非阻塞操作、多路复用等
1
2
3
4
5
6
7
8
select {
case msg1 := <-ch1:
fmt.Println("从 ch1 接收到:", msg1)
case ch2 <- 10:
fmt.Println("向 ch2 发送了 10")
default:
fmt.Println("没有任何 channel 准备好,执行默认分支")
}

结合超时实现非阻塞等待

1
2
3
4
5
6
select {
case msg := <-ch:
fmt.Println("收到消息:", msg)
case <-time.After(1 * time.Second):
fmt.Println("等待超时")
}

线程安全

如果是同步执行,那么最后count是0;如果给add和sub是异步执行,最后count不会是0, 而且每次都不一样

由于add和div同时操作了count,count为10的时候,add后结果为11,sub后结果为9,如果sub在add后面1微秒执行,那么count就被改成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
27
28
29
30
package main

import (
"fmt"
"sync"
)

var count int
var wait sync.WaitGroup

func add() {
for i := 0; i < 100000; i++ {
count++
}
wait.Done()
}
func sub() {
for i := 0; i < 100000; i++ {
count--
}
wait.Done()
}

func main() {
wait.Add(2)
go add()
go sub()
wait.Wait()
fmt.Println(count)
}

使用互斥锁(sync.Mutex)保护共享变量, 在需要修改共享变量时候获取锁,修改完成后解锁

1
2
3
4
5
6
7
8
9
var mu sync.Mutex
func add() {
for i := 0; i < 100000; i++ {
mu.Lock()
count++
mu.Unlock()
}
wait.Done()
}

异常处理

go中处理错误的方式有点奇怪, 一般函数都返回两个返回值,一个结果,一个error;遇到错误向上抛错误,被调用的函数不处理错误

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

import (
"errors"
"fmt"
)

func div(a, b float32) (res float32, err error) {
if b == 0 {
err = errors.New("除数不能为0")
return
}
res = a / b
return
}

func run() (res float32, err error) {
res, err = div(9, 5)
if err != nil {
return
}
return res, nil
}

func main() {
res, err := run()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("结果为", res)
}

// 输出 除数不能为0

如果遇到了致命错误,可以使用panic抛出致命错误,来中断程序运行

1
2
3
4
5
6
7
func main() {
_, err := os.ReadFile("1234")
if err != nil {
panic(err)
}
fmt.Println("main")
}

但有的函数就是会抛出panic, 但又不想主线程停止,可以捕获panic后恢复程序运行,在基础篇也有写

1
2
3
4
5
6
7
8
9
10
11
12
13
func div(a, b int) int {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
debug.PrintStack() // 打印错误堆栈
}
}()
return a / b
}
func main() {
div(10, 0)
fmt.Println("继续执行")
}

泛型

泛型就是任意类型, python中如果不指定类型,就是泛型,传递什么类型都可以,但是Go是强类型语言,必须定义参数类型

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"
)
// 使用any写一个python的print函数,支持任意类型的值打印。
func print(a ...any) {
fmt.Println(a...) // 将切片 a 展开成多个参数传入
}

type User struct {
Name string
Age int
}

func main() {
u := User{
Name: "枫枫",
Age: 21,
}
print(1, "1", 3.14, u)
}

如果是函数是计算类型的函数,那么any就不好用了, 结构体是不能相加的,所有可以用自定义接口来选择所有的数字类型

1
2
3
4
5
6
7
8
9
10
11
12
type Number interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

func push[T Number](a, b T) T {
return a + b
}

func main() {
print(push(0.2, 0.1))
}
// 输出 0.30000000000000004

序列化和反序列化

前端传递标准json格式的文本给后端,后端拿到的数据就是字符串,使用json字符串序列化可以获得对象,直接使用结构体属性取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"encoding/json"
"fmt"
)

type Request[T any] struct {
Req string `json:"req"`
ReqId string `json:"req_id"`
Data T `json:"data"`
}

type UserInfo struct {
Address string `json:"address"`
Phone string `json:"phone"`
}

type User struct {
Name string `json:"name"`
Age int `json:"age"`
UserInfo UserInfo `json:"info"`
}

func main() {
user := User{
Name: "枫枫",
Age: 18,
UserInfo: UserInfo{
Address: "北京",
Phone: "13888******",
},
}
request := Request[User]{
Req: "login",
ReqId: "1000000001",
Data: user,
}
fmt.Println(request)
r1, _ := json.Marshal(request) // 序列化
fmt.Println(string(r1))
jsonStr := `{"req":"login","req_id":"1000000001","data":{"name":"枫枫","age":18,"info":{"Address":"北京","Phone":"13888******"}}}`
var r2 Request[User]
err := json.Unmarshal([]byte(jsonStr), &r2) // 反序列化
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r2)
}

// 输出
// {login 1000000001 {枫枫 18 {北京 13888******}}}
// {"req":"login","req_id":"1000000001","data":{"name":"枫枫","age":18,"info":{"address":"北京","phone":"13888******"}}}
// {login 1000000001 {枫枫 18 {北京 13888******}}}

文件操作

在Go中读取文件很简单,使用os.ReadFile就能读取全部文件,返回[]byte, 它会自动关闭句柄

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

import (
"fmt"
"os"
)

func main() {
byteData, err := os.ReadFile("file1.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println(string(byteData))
}

分块读取文件,可以参考一下os.ReadFile的代码,这里就不写了,我不会

常用的是按行读取,就像python的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
package main

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

func main() {
f, _ := os.Open("file.txt")
defer func() {
err := f.Close()
if err != nil {
fmt.Println("Close Err: ", err)
}
}()
buf := bufio.NewReader(f)
for true {
line, _, err := buf.ReadLine()
// 按照指定分割符读取一行, 其实ReadLine内部就是'\n'
// line, err := buf.ReadSlice('\n')
if err == io.EOF {
break
}
fmt.Println(string(line), err)
}
}

完整文件写入就很简单,也很常用,使用官方的os.WriteFile就可以实现,os.WriteFile接收三个参数

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

import (
"os"
)

func main() {
byteData := []byte("你好\n世界")
os.WriteFile("file1.txt", byteData, 744)
}

递归遍历指定目录下的所有文件是经常使用的功能,Go基础库也封装了类似的方法。

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

import (
"fmt"
"io/fs"
"path/filepath"
)

func main() {
// 只想输出当前目录的话可以使用os.ReadDir
filepath.WalkDir("./", func(path string, d fs.DirEntry, err error) error {
fmt.Println(path)
return nil
})
}

反射 (高阶)

Go 的反射(reflection)是指在运行时动态地检查、修改变量的类型和值的能力,主要通过 reflect 包实现。

反射运行时性能开销较大,一般在需要“动态性”的地方使用,普通业务中尽量避免滥用。

对于结构体字段的修改,需要 reflect.Value 是可写的(比如传入 &struct 而不是 struct)。

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

import (
"fmt"
"reflect"
)

type User struct {
Name string `json:"name"`
Age int `json:"age"`
Man bool
}

func main() {
user := User{
Name: "张三",
Age: 18,
Man: true,
}
t := reflect.TypeOf(user)
v := reflect.ValueOf(user)
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
tag := field.Tag.Get("json")
if tag == "" {
tag = field.Name
}
fmt.Printf("字段名: %s, 类型: %s, 值: %v tag: %s\n", field.Name, field.Type, value, tag)
}
}

写了简单的拼接sql的小脚本,不防sql注入和复杂sql语句

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

type Model struct {
Id int `my-orm:"id"`
Name string `my-orm:"name"`
}

func Select(obj any, query string, args ...any) {
var builder strings.Builder
defer builder.Reset()
var filed []string
t := reflect.TypeOf(obj)
sql := "select %v from %v where %v;"
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Tag.Get("my-orm")
filed = append(filed, name)
}
argIndex := 0
for i := 0; i < len(query); i++ {
if query[i] == '?' {
switch args[argIndex].(type) {
case string:
builder.WriteString(fmt.Sprintf("'%s'", args[argIndex]))
default:
builder.WriteString(fmt.Sprintf("%v", args[argIndex]))
}
argIndex++
} else {
builder.WriteByte(query[i])
}
}
where := builder.String()
if where == "" {
where = "true"
}
sql = fmt.Sprintf(sql, strings.Join(filed, ","), strings.ToLower(t.Name()), where)
fmt.Println(sql)
}
func main() {
model := Model{}
Select(model, "name = ?", "xpctf")
// select id, name from model where name = 'xpctf';
Select(model, "id = ? and name = ??", 1, "xpctf")
// select id, name from model where id = 1 and name = 'xpctf';
Select(model, "")
// select id, name from model;
}

网络编程

在Go中使用net基础库来实现网络编程, net基础库实现的网络通信的底层,主要是值传输层协议(TCP/UDP),像gin是框架是使用应用层的Http协议。

这里我懒得写笔记了,因为网络编程很少使用,我的目的是学习gin框架,贴出gpt生成的代码,以后想要学习可以去看枫枫老师的课程网络编程TCP

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
// tcp server
package main

import (
"fmt"
"net"
)

func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
fmt.Println("服务器启动,监听 8080 端口")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println("连接失败:", err)
continue
}
go handleConn(conn)
}
}

func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println("收到:", string(buf[:n]))
conn.Write([]byte("你好,我是服务器\n"))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// tcp client 
package main

import (
"fmt"
"net"
)

func main() {
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
panic(err)
}
defer conn.Close()
conn.Write([]byte("你好,服务器"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println("收到回复:", string(buf[:n]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// udp server
package main

import (
"fmt"
"net"
)

func main() {
addr, _ := net.ResolveUDPAddr("udp", ":9999")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()
fmt.Println("UDP 服务器已启动")
buf := make([]byte, 1024)
for {
n, remoteAddr, _ := conn.ReadFromUDP(buf)
fmt.Printf("收到来自 %s 的消息: %s\n", remoteAddr, string(buf[:n]))
conn.WriteToUDP([]byte("收到"), remoteAddr)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// udp client
package main

import (
"fmt"
"net"
)

func main() {
conn, _ := net.Dial("udp", "localhost:9999")
defer conn.Close()
conn.Write([]byte("你好 UDP 服务器"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println("收到回复:", string(buf[:n]))
}

补充说明

  • 使用 TCP 时,推荐用 go handleConn(conn) 让每个连接独立处理。
  • UDP 是无连接的,适合轻量、实时的通信。
  • 所有读取操作(Read)都有阻塞性,要考虑加超时或使用 select