一、rune类型
rune
是 Go 语言特有的数据类型,专门用于处理 Unicode 字符,尤其是多字节字符(如中文、日文、韩文等)。理解 rune
对于正确处理国际化文本至关重要。
在go中,rune本质是int32类型。它的作用是表示一个 Unicode 码点(code point),范围从 0
到 0x10FFFF
,可以表示世界上几乎所有的字符。
(1)为什么需要rune类型
Go 中的 string
是字节的集合,每个 byte
占 1 个字节(8 位),只能表示 ASCII 字符(0-127)。而 Unicode 字符(如中文)通常需要多个字节表示(UTF-8 编码下占 3 个字节),因此直接用 byte
处理会导致错误。
直接用 byte
处理中文的问题
Go
package main
import "fmt"
func main() {
s := "你好,世界"
fmt.Println("字符串长度(字节数):", len(s)) // 输出:15(每个中文占3字节,标点也占3字节)
// 尝试用索引访问字符(错误方式)
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i]) // 输出乱码:ä ½ å ¥½ ¼ ä ¸ ç �
}
}
len(s)
返回的是字符串的字节数,而非字符数;直接通过索引访问得到的是单个字节,而非完整字符。
(2)将字符串转换为 rune
切片
通过 []rune(s)
可以将字符串转换为 rune
切片,每个元素对应一个 Unicode 字符:
Go
package main
import "fmt"
func main() {
s := "你好,世界"
runes := []rune(s) // 转换为 rune 切片
fmt.Println("字符数:", len(runes)) // 输出:5(正确的字符数量)
// 遍历 rune 切片(正确访问每个字符)
for i, r := range runes {
fmt.Printf("索引 %d: %c(Unicode码点:%U)\n", i, r, r)
}
}
字符数: 5
索引 0: 你(Unicode码点:U+4F60)
索引 1: 好(Unicode码点:U+597D)
索引 2: ,(Unicode码点:U+FF0C)
索引 3: 世(Unicode码点:U+4E16)
索引 4: 界(Unicode码点:U+754C)
(3)for range
循环遍历字符串
for range
循环会自动将字符串按 rune
解析,每次迭代返回一个 Unicode 字符:
Go
package main
import "fmt"
func main() {
s := "Go语言❤️"
// for range 自动处理 rune
for i, r := range s {
fmt.Printf("位置 %d: 字符 %c(占 %d 字节)\n", i, r, len(string(r)))
}
}
位置 0: 字符 G(占 1 字节)
位置 1: 字符 o(占 1 字节)
位置 2: 字符 语(占 3 字节)
位置 5: 字符 言(占 3 字节)
位置 8: 字符 ❤(占 3 字节)
位置 11: 字符 ️(占 3 字节)
(4)处理特殊字符(如 emoji、组合字符)
Go
package main
import "fmt"
func main() {
// 笑脸 emoji(占4字节)
smile := '😊'
fmt.Printf("笑脸:%c,类型:%T,Unicode码点:%U,字节数:%d\n",
smile, smile, smile, len(string(smile)))
// 组合字符(e + 重音符号 → é)
eAcute := 'é'
fmt.Printf("组合字符:%c,Unicode码点:%U\n", eAcute, eAcute)
}
笑脸:😊,类型:int32,Unicode码点:U+1F60A,字节数:4
组合字符:é,Unicode码点:U+00E9
(5)字符串截取(避免截断多字节字符)
直接通过字节索引截取字符串可能截断多字节字符,使用 rune
可以安全截取
Go
package main
import "fmt"
// 安全截取字符串前n个字符
func safeSubstring(s string, n int) string {
runes := []rune(s)
if n > len(runes) {
n = len(runes)
}
return string(runes[:n])
}
func main() {
s := "Hello,世界!"
// 错误方式:按字节截取(可能截断)
badSub := s[:7]
fmt.Println("错误截取:", badSub) // 输出:Helloï¼(乱码)
// 正确方式:按 rune 截取
goodSub := safeSubstring(s, 7)
fmt.Println("正确截取:", goodSub) // 输出:Hello,世
}
二、Go 语言中结构体 Tag 解析
在 Go 语言中,结构体标签(Struct Tag)是一种附着在结构体字段上的元数据,常用于 JSON 序列化、数据库映射等场景。而解析 Tag 的核心技术是 反射(Reflection),它允许程序在运行时检查和操作变量的类型与值。
(1)结构体 Tag 基础
结构体 Tag 是字段声明后用反引号 ````` 包裹的字符串,格式通常为 key:"value" key2:"value2"
Go
package main
import "fmt"
// 定义带 Tag 的结构体
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age,omitempty" db:"user_age"`
}
func main() {
u := User{Name: "Alice", Age: 30}
fmt.Printf("%+v\n", u) // 输出:{Name:Alice Age:30}
}
Tag 本身不影响结构体的字段值,仅作为元数据存在,一个字段可以有多个 Tag(用空格分隔不同 key),常见 Tag 用途:json
(序列化字段名)、db
(数据库列名)、xml
(XML 节点名)等。
(2)Tag 依靠反射解析
反射的核心是两个类型:
reflect.Type
:表示变量的类型信息(静态类型)reflect.Value
:表示变量的值信息(动态值)
通过 reflect.TypeOf()
和 reflect.ValueOf()
可获取任意变量的反射对象
Go
package main
import "reflect"
func main() {
u := User{Name: "Bob", Age: 25}
// 获取类型信息(reflect.Type)
t := reflect.TypeOf(u)
fmt.Println("类型:", t.Name()) // 输出:User
// 获取值信息(reflect.Value)
v := reflect.ValueOf(u)
fmt.Println("值:", v.Field(0).String()) // 输出:Bob
}
解析结构体字段的 Tag 需通过 reflect.Type
的 Field()
方法获取字段信息,再通过 Tag.Get(key)
提取指定 key 的值
Go
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age,omitempty" db:"user_age"`
}
func parseTags(s interface{}) {
// 1. 获取反射类型对象(必须传入指针,否则无法修改值,但解析Tag无需修改)
t := reflect.TypeOf(s)
// 2. 检查是否为结构体(若传入指针,需先获取元素类型)
if t.Kind() == reflect.Ptr {
t = t.Elem() // 解引用指针,获取结构体类型
}
if t.Kind() != reflect.Struct {
fmt.Println("输入不是结构体或结构体指针")
return
}
// 3. 遍历结构体字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 获取第i个字段的类型信息
fmt.Printf("字段名:%s\n", field.Name)
// 4. 解析Tag
jsonTag := field.Tag.Get("json") // 获取json标签
dbTag := field.Tag.Get("db") // 获取db标签
fmt.Printf(" json标签:%s\n", jsonTag)
fmt.Printf(" db标签:%s\n", dbTag)
}
}
func main() {
u := User{}
parseTags(&u) // 传入指针(推荐方式)
// 输出:
// 字段名:Name
// json标签:name
// db标签:user_name
// 字段名:Age
// json标签:age,omitempty
// db标签:user_age
}
reflect.Type.Field(i)
:获取结构体第i
个字段的StructField
对象(包含字段名、类型、Tag 等信息)StructField.Tag
:字段的 Tag 信息(类型为reflect.StructTag
)StructTag.Get(key)
:提取 Tag 中指定 key 的值(自动忽略空格和引号)
reflect.StructTag.Get(key)
的底层逻辑是解析 Tag 字符串,按规则提取值。简化的实现思路如下
Go
func getTag(tag string, key string) string {
// Tag 格式示例:`json:"name" db:"user_name"`
// 1. 按空格分割多个 key-value 对
// 2. 对每个部分解析为 key:"value" 格式
// 3. 匹配 key 并返回对应的 value(去除引号)
// 实际实现需处理各种边缘情况(如引号内的空格、转义字符等)
return "" // 实际逻辑见 Go 源码:src/reflect/type.go
}
三、反射的核心原理
反射的本质是程序在运行时对自身结构的检查能力,Go 语言的反射系统建立在类型系统之上,核心原理包括
(1)类型信息的存储
Go 语言中,每个变量在编译时都会被赋予一个类型信息,存储在可执行文件中。运行时,通过 reflect.TypeOf
可以获取这个类型信息的副本(reflect.Type
)。
- 静态类型:变量声明时的类型(如
var a int
中int
是静态类型) - 动态类型:接口变量实际指向的值的类型(仅接口有动态类型)
(2)反射的三大法则
- 从接口值到反射对象
Go
var x int = 10
t := reflect.TypeOf(x) // t 是 int 类型的反射对象
v := reflect.ValueOf(x) // v 是 10 这个值的反射对象
- 从反射对象到接口值
Go
v := reflect.ValueOf(10)
x := v.Interface().(int) // 类型断言,恢复为 int
- 要修改反射对象,其值必须可设置
反射对象可设置的前提:它持有原始变量的地址(即通过指针获取的反射对象)
Go
var x int = 10
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // 输出:false(v 是副本,不可修改)
vPtr := reflect.ValueOf(&x).Elem() // 解引用指针,获取原始值的反射对象
fmt.Println(vPtr.CanSet()) // 输出:true(可修改)
vPtr.SetInt(20)
fmt.Println(x) // 输出:20(原始变量被修改)
四、goroutine什么情况下会阻塞
goroutine
是轻量级线程,由 Go 运行时(而非操作系统)调度。虽然 goroutine
设计初衷是高效并发,但在多种场景下会进入阻塞状态 (暂停执行,等待特定条件满足后恢复)。以下是 goroutine
可能阻塞的所有常见情况。
(1)通道操作导致的阻塞
通道是 goroutine
间通信的主要方式,未正确处理的通道操作是阻塞的最常见原因。
1、向无缓冲通道发送数据(ch <- data
)
阻塞条件 :无缓冲通道(make(chan T)
)发送数据时,必须有对应的接收操作(<-ch
),否则发送方 goroutine
会阻塞,直到有 goroutine
接收数据
Go
package main
import "fmt"
func main() {
ch := make(chan int) // 无缓冲通道
// 启动发送 goroutine
go func() {
fmt.Println("发送前")
ch <- 10 // 阻塞:等待接收方
fmt.Println("发送后") // 接收方处理后才执行
}()
// 延迟接收(模拟处理耗时)
// 若注释掉下面一行,发送方会永久阻塞(导致死锁)
fmt.Println("接收值:", <-ch)
}
发送前
接收值:10
发送后
2、从无缓冲通道接收数据(data := <-ch
)
阻塞条件 :无缓冲通道接收数据时,若通道中没有数据,接收方 goroutine
会阻塞,直到有 goroutine
发送数据。
Go
package main
import "fmt"
func main() {
ch := make(chan int) // 无缓冲通道
// 启动接收 goroutine
go func() {
fmt.Println("接收前")
val := <-ch // 阻塞:等待发送方
fmt.Println("接收值:", val)
}()
// 延迟发送(模拟处理耗时)
ch <- 20 // 发送数据,唤醒接收方
}
接收前
接收值:20
3、向缓冲通道发送数据(缓冲区已满)
阻塞条件 :缓冲通道(make(chan T, n)
)的缓冲区已满时,发送操作会阻塞,直到有 goroutine
从通道接收数据(腾出缓冲区空间)。
Go
package main
import "fmt"
import "time"
func main() {
ch := make(chan int, 2) // 缓冲区大小为 2
// 填充缓冲区(不阻塞)
ch <- 1
ch <- 2
// 启动发送 goroutine(缓冲区已满,会阻塞)
go func() {
fmt.Println("尝试发送第3个值")
ch <- 3 // 阻塞:缓冲区已满
fmt.Println("第3个值发送成功") // 有接收后才执行
}()
// 接收一个值(腾出缓冲区空间)
time.Sleep(3 * time.Second)
fmt.Println("接收值:", <-ch) // 接收 1
time.Sleep(3 * time.Second)
}
尝试发送第3个值
接收值: 1
第3个值发送成功
4、从缓冲通道接收数据(缓冲区为空)
阻塞条件 :缓冲通道的缓冲区为空时,接收操作会阻塞,直到有 goroutine
向通道发送数据。
Go
package main
import "fmt"
import "time"
func main() {
ch := make(chan int, 1) // 缓冲通道,初始为空
// 启动接收 goroutine(缓冲区为空,阻塞)
go func() {
fmt.Println("等待接收")
val := <-ch // 阻塞:缓冲区为空
fmt.Println("接收到:", val)
}()
time.Sleep(1 * time.Second)
// 发送数据(唤醒接收方)
ch <- 30
time.Sleep(1 * time.Second)
}
等待接收
接收到: 30
(2)同步原语导致的阻塞
Go 标准库 sync
包提供的同步工具(如锁、等待组)会导致 goroutine
阻塞,直到同步条件满足。
1、互斥锁(sync.Mutex
)与读写锁(sync.RWMutex
)
Mutex.Lock()
阻塞 :当锁已被其他goroutine
持有,调用Lock()
的goroutine
会阻塞,直到锁被释放(Unlock()
)。RWMutex.Lock()
阻塞:写锁被持有或有读锁时,后续写锁请求会阻塞。RWMutex.RLock()
阻塞:写锁被持有,后续读锁请求会阻塞。
Go
package main
import (
"fmt"
"sync"
"time"
)
var mu sync.Mutex
func main() {
// 第一个 goroutine 持有锁
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("goroutine1: 获得锁,开始处理")
time.Sleep(2 * time.Second) // 模拟耗时操作
fmt.Println("goroutine1: 释放锁")
}()
// 第二个 goroutine 尝试获取锁(会阻塞)
go func() {
fmt.Println("goroutine2: 尝试获取锁")
mu.Lock() // 阻塞:锁被 goroutine1 持有
defer mu.Unlock()
fmt.Println("goroutine2: 获得锁,开始处理")
}()
time.Sleep(3 * time.Second) // 等待所有 goroutine 执行
}
输出顺序不一定完全和我这个一样:
goroutine1: 获得锁,开始处理
goroutine2: 尝试获取锁
goroutine1: 释放锁
goroutine2: 获得锁,开始处理
2、等待组(sync.WaitGroup
)
WaitGroup.Wait()
阻塞 :调用 Wait()
的 goroutine
会阻塞,直到等待组的计数器减为 0(所有 Add(n)
的 goroutine
都调用了 Done()
)。
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 等待 2 个 goroutine
// 启动第一个任务
go func() {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("任务1完成")
}()
// 启动第二个任务
go func() {
defer wg.Done()
time.Sleep(2 * time.Second)
fmt.Println("任务2完成")
}()
fmt.Println("等待所有任务完成...")
wg.Wait() // 阻塞:等待计数器为 0
fmt.Println("所有任务完成")
}
等待所有任务完成...
任务一完成
任务二完成
所有任务完成
3、条件变量(sync.Cond
)
Cond.Wait()
阻塞 :调用 Wait()
的 goroutine
会释放关联的锁并阻塞,直到被 Signal()
或 Broadcast()
唤醒(唤醒后需重新获取锁)。
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
mu := &sync.Mutex{}
cond := sync.NewCond(mu)
ready := false
// 等待者 goroutine
go func() {
cond.L.Lock()
defer cond.L.Unlock()
// 循环等待条件满足(避免虚假唤醒)
for !ready {
fmt.Println("等待者:条件不满足,阻塞")
cond.Wait() // 释放锁并阻塞
}
fmt.Println("等待者:条件满足,继续执行")
}()
// 通知者 goroutine
go func() {
time.Sleep(1 * time.Second) // 模拟准备时间
cond.L.Lock()
ready = true // 满足条件
cond.L.Unlock()
fmt.Println("通知者:发送唤醒信号")
cond.Signal() // 唤醒一个等待的 goroutine
}()
time.Sleep(2 * time.Second)
}
等待者:条件不满足,阻塞
通知者:发送唤醒信号
等待者:条件满足,继续执行
cond.Wait()
内部做了三件事:
a. 释放已持有的锁 (cond.L.Unlock()
)。
b. 阻塞当前 goroutine,等待通知。
c. 当被唤醒时,重新获取锁 (cond.L.Lock()
)。
cond.Signal()
唤醒的是谁?
cond.Signal()
会唤醒正在等待(即正在执行 cond.Wait()
)的 goroutine 中的任意一个,这些 goroutine 都绑定在同一个 cond
变量上。它唤醒的不是"拥有这个cond的goroutine",而是"等待这个cond的goroutine"。
拥有者 :条件变量本身没有"拥有者"的概念。锁 (mutex
) 有"持有者",但条件变量没有。
等待队列 :每个 sync.Cond
实例内部维护了一个等待队列 ,所有在这个 cond
上调用 Wait()
的 goroutine 都会被加入到这个队列中。
cond.Signal()
的工作流程是:
-
从与该
cond
关联的等待队列中,取出第一个(Go 运行时不保证是严格FIFO,但可以理解为最先等待的或任意一个)goroutine。 -
将这个 goroutine 标记为可唤醒状态。
下面给出有多个等待者的情况:
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
mu := &sync.Mutex{}
cond := sync.NewCond(mu)
ready := false
// 创建 3 个等待者 goroutine
for i := 1; i <= 3; i++ {
id := i
go func() {
cond.L.Lock()
defer cond.L.Unlock()
for !ready {
fmt.Printf("等待者 %d:条件不满足,阻塞\n", id)
cond.Wait()
fmt.Printf("等待者 %d:被唤醒,检查条件\n", id)
}
fmt.Printf("等待者 %d:条件满足,继续执行\n", id)
}()
}
// 通知者 goroutine
go func() {
time.Sleep(1 * time.Second)
cond.L.Lock()
ready = true
cond.L.Unlock()
fmt.Println("通知者:发送唤醒信号 (Signal)")
cond.Signal() // 只唤醒一个等待者!
}()
time.Sleep(3 * time.Second)
fmt.Println("\n--- 使用 Broadcast 的例子 ---")
// 重置,演示 Broadcast
mu2 := &sync.Mutex{}
cond2 := sync.NewCond(mu2)
ready2 := false
for i := 1; i <= 3; i++ {
id := i
go func() {
cond2.L.Lock()
defer cond2.L.Unlock()
for !ready2 {
fmt.Printf("等待者 %d:条件不满足,阻塞\n", id)
cond2.Wait()
fmt.Printf("等待者 %d:被唤醒,检查条件\n", id)
}
fmt.Printf("等待者 %d:条件满足,继续执行\n", id)
}()
}
time.Sleep(500 * time.Millisecond)
go func() {
time.Sleep(1 * time.Second)
cond2.L.Lock()
ready2 = true
cond2.L.Unlock()
fmt.Println("通知者:发送广播信号 (Broadcast)")
cond2.Broadcast() // 唤醒所有等待者!
}()
time.Sleep(3 * time.Second)
}
等待者 1:条件不满足,阻塞
等待者 2:条件不满足,阻塞
等待者 3:条件不满足,阻塞
通知者:发送唤醒信号 (Signal)
等待者 1:被唤醒,检查条件
等待者 1:条件满足,继续执行
--- 使用 Broadcast 的例子 ---
等待者 3:条件不满足,阻塞
等待者 2:条件不满足,阻塞
等待者 1:条件不满足,阻塞
通知者:发送广播信号 (Broadcast)
等待者 1:被唤醒,检查条件
等待者 1:条件满足,继续执行
等待者 3:被唤醒,检查条件
等待者 3:条件满足,继续执行
等待者 2:被唤醒,检查条件
等待者 2:条件满足,继续执行
当使用 cond.Signal()
时,只有一个等待者会被唤醒并继续执行。
当使用 cond.Broadcast()
时,所有三个等待者都会被唤醒并继续执行。
(3)网络与 I/O 操作导致的阻塞
goroutine
执行网络请求、文件读写等 I/O 操作时,会进入阻塞状态,直到 I/O 完成。
1、网络请求(HTTP、TCP 等)
Go
package main
import (
"fmt"
"net/http"
)
func main() {
// 启动 goroutine 发起网络请求(会阻塞)
go func() {
fmt.Println("开始请求网页...")
resp, err := http.Get("https://www.baidu.com") // 阻塞:等待响应
if err == nil {
fmt.Printf("请求完成,状态码:%d\n", resp.StatusCode)
resp.Body.Close()
}
}()
// 等待请求完成
var input string
fmt.Scanln(&input) // 阻塞主线程,避免程序退出
}
开始请求网页...
请求完成,状态码:200
2、文件读写
阻塞条件 :读写文件时,goroutine
会阻塞,直到操作完成(数据读取 / 写入完毕)。
Go
package main
import (
"fmt"
"os"
)
func main() {
// 启动 goroutine 读取文件(会阻塞)
go func() {
fmt.Println("开始读取文件...")
data, err := os.ReadFile("test.txt") // 阻塞:等待文件读取完成
if err == nil {
fmt.Printf("文件读取完成,长度:%d 字节\n", len(data))
} else {
fmt.Println("读取错误:", err)
}
}()
// 等待操作完成
var input string
fmt.Scanln(&input)
}
(4)其他阻塞场景
1、time.Sleep()
阻塞条件 :goroutine
会暂停执行指定的时间,期间处于阻塞状态。
Go
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("开始休眠...")
time.Sleep(2 * time.Second) // 阻塞 2 秒
fmt.Println("休眠结束")
}()
time.Sleep(3 * time.Second) // 等待子 goroutine 执行
}
2、等待操作系统信号(os.Signal
)
Go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 创建信号通道
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // 监听 Ctrl+C 等信号
// 启动 goroutine 等待信号(阻塞)
go func() {
fmt.Println("等待信号(按 Ctrl+C 发送 SIGINT)...")
sig := <-sigChan // 阻塞:等待信号
fmt.Printf("收到信号:%v,退出程序\n", sig)
os.Exit(0)
}()
// 主线程无限循环
for {
time.Sleep(1 * time.Second)
}
}
// 等待信号(按 Ctrl+C 发送 SIGINT)...
// ^C收到信号:interrupt,退出程序