结构体 在 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)) }
使用-
可以隐藏一个字段,比如password字段不想返回给前端
使用omitempty
可以忽略空值, 比如:json:"age,omitempty"
自定义数据类型 Go 的自定义数据类型 ,是指使用 type
关键字给现有类型 起一个新名字 ,或者定义一个全新的结构体、接口等类型
1 2 type MyInt int type MyString = 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)) }
使用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 mainimport ( "fmt" "sync" "time" ) var wait sync.WaitGroupfunc 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 mainimport ( "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) for i := 1 ; i <= 3 ; i++ { msg := <-ch fmt.Println("收到消息:" , msg) } fmt.Println("狗叫完成" , time.Since(startTime)) }
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 mainimport ( "fmt" "sync" ) var count int var wait sync.WaitGroupfunc 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.Mutexfunc 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 mainimport ( "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) }
如果遇到了致命错误,可以使用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 mainimport ( "fmt" ) func print (a ...any) { fmt.Println(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 )) }
序列化和反序列化 前端传递标准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 mainimport ( "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) }
文件操作 在Go中读取文件很简单,使用os.ReadFile
就能读取全部文件,返回[]byte, 它会自动关闭句柄
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "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 mainimport ( "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() 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 mainimport ( "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 mainimport ( "fmt" "io/fs" "path/filepath" ) func main () { 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 mainimport ( "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 mainimport ( "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(model, "id = ? and name = ??" , 1 , "xpctf" ) Select(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 package mainimport ( "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 package mainimport ( "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 package mainimport ( "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 package mainimport ( "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
。