一、RPC 框架
RPC 框架常见的通信方式有 简单 RPC、服务端流式 RPC、客户端流式 RPC、双向流式 RPC。
简单 RPC 是最基本的 RPC 形式,客户端发送一个请求到服务器,并等待响应。这种模式是同步的,即客户端在接收到服务器响应之前不会执行其他操作。
示例:
假设有一个远程服务,提供天气查询功能。客户端发送一个包含城市名的请求,服务器处理请求并返回该城市的天气信息。
客户端: 发送 "获取北京天气"
服务器: 接收请求并处理
服务器: 返回 "北京天气晴朗"
客户端: 接收并显示天气信息
服务端流式 RPC 允许服务器向客户端发送多个连续的消息。这是一种单向流,客户端发起请求后,可以接收一系列来自服务器的响应。
示例:
一个股票实时报价服务,客户端请求某股票的实时价格,服务器则定期推送最新的股价信息。
客户端: 请求 "订阅股票 XYZ 的实时价格"
服务器: 定期发送更新的股价信息
服务器: "XYZ 现价 100"
服务器: "XYZ 现价 101"
服务器: "XYZ 现价 102"
...
客户端流式 RPC 允许客户端向服务器发送一系列消息,而服务器在所有消息接收完毕后返回一个响应。这适用于需要批量处理数据的场景。
示例:
一个文档分析服务,客户端将文档分成多个部分连续发送给服务器,服务器在接收完所有部分后,返回分析结果。
客户端: 发送文档第一部分
客户端: 发送文档第二部分
客户端: 发送文档第三部分
...
服务器: 接收所有部分并处理
服务器: 返回处理结果 "文档分析完成,主题为..."
双向流式 RPC 允许客户端和服务器之间进行全双工通信,即双方都可以在任何时候发送和接收消息。这种方式适用于需要高度交互的应用场景。
示例:
一个在线教育平台的实时互动课程,学生和教师可以互发消息和反馈。
教师: 发送 "开始课程,今天我们学习RPC"
学生: 发送 "我有个问题,RPC是什么?"
教师: 回复 "RPC是远程过程调用,是一种..."
学生: 发送 "明白了,谢谢!"
教师: 发送 "接下来我们看一个示例..."
...
这些通信方式使得 RPC 框架能够适应各种不同的应用场景,从简单的数据请求到复杂的实时数据流处理。
二、验证中英文
处理文件上传 · Build web application with Golang
// 验证中文
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
return false
}
// 验证英文
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
return false
}
// 验证email
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
fmt.Println("no")
}else{
fmt.Println("yes")
}
// 验证手机号
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
return false
}
// 验证身份证
//验证15位身份证,15位的是全部数字
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
return false
}
//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
return false
}
三、sync.Once
实现模拟
在Go语言中,sync.Once
是一个用于确保某个函数只执行一次的同步原语。它通常用于初始化操作,比如初始化一个全局变量或执行一次性的设置。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// 自定义的 Once 类型
type Once struct {
done uint32 // 使用 uint32 作为标志位,表示函数是否已执行
m sync.Mutex // 互斥锁,用于保护 done 变量
}
// Do 方法确保传入的函数 f 只执行一次
func (o *Once) Do(f func()) {
// 使用 atomic.LoadUint32 检查 done 是否为 1(表示函数已执行)
if atomic.LoadUint32(&o.done) == 1 {
return
}
// 如果 done 不是 1,则尝试加锁并执行函数
o.m.Lock()
defer o.m.Unlock()
// 再次检查 done,因为可能有其他 goroutine 在我们获取锁之前已经执行了函数
if atomic.LoadUint32(&o.done) == 0 {
// 标记为已执行
atomic.StoreUint32(&o.done, 1)
// 执行函数
f()
}
}
func main() {
var once Once
once.Do(func() {
fmt.Println("Function executed once")
})
// 再次调用 Do,但函数不会再次执行
once.Do(func() {
fmt.Println("This will not be printed")
})
}
四 、go远程调试
# 调试脚本
# launch.json文件
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch file",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${file}",
"showLog": true,
"env": {},
"cwd": "${workspaceFolder}"
},
{
"name": "Connect to server",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/home/project", // 项目路径
"port": 8349, // dlv服务所监听的端口
"host": "10.23.14.19", // 项目运行所在机器的ip地址
"apiVersion": 2
}
]
}
dlv exec --headless --listen=:8349 --api-version=2 --accept-multiclient ./bin/cli -- settle check // 使用dlv exec 执行已经编译好的脚本 cli -- settle check
然后点击vscode的调试按钮,从下拉选项中选择"Connect to server"
如果不是调试脚本,调试项目,
dlv debug --headless --listen=:8349 --api-version=2 -- -conf ./conf/app.toml
如果不需要指定配置文件,可取消--及其后面的参数
如果有需要也可以调试正在运行的代码
dlv attach <PID> --headless --listen=:8349 --api-version=2
四、go语言调试时报错
(unreadable could not find loclist entry at 0x898a5f for address 0x9f29d0),
这个错误信息表明你在使用 Go 语言进行调试时遇到了 DWARF 调试信息相关的问题。具体错误 "could not find loclist entry at 0x898a5f for address 0x9f29d0"
通常发生在使用调试器(如 GDB 或 Delve)时,调试器无法正确解析程序的调试信息。
解决方法:编译时使用 -gcflags="all=-N -l"
参数来禁用优化并保留完整的调试信息
go build -gcflags="all=-N -l" your_program.go
"[builder] the value of \"xxx in\" must be of []interface{} type"报错:
这个错误 "[builder] the value of \"xxx in\" must be of []interface{} type"
通常出现在 Go 代码中使用 SQL 构建器(如 gorm
、sqlx
、squirrel
或其他 ORM/Query Builder)时,传入 IN
子句的参数类型不正确。
IN
子句在 SQL 中用于匹配多个值,例如:
SELECT * FROM users WHERE id IN (1, 2, 3);
在 Go 中,SQL 构建器通常要求 IN
的参数是 []interface{}
(即 []any
),而不是 []int
、[]string
或其他具体类型的切片。
如果你的代码类似:
ids := []int{1, 2, 3}
db.Where("id IN (?)", ids) // 错误!不能直接传 []int
就会触发这个错误,因为 ids
是 []int
,而不是 []interface{}
。
解决方法:
(1)手动转换为 []interface{}
ids := []int{1, 2, 3}
// 转换为 []interface{}
var interfaceIDs []interface{}
for _, id := range ids {
interfaceIDs = append(interfaceIDs, id)
}
db.Where("id IN (?)", interfaceIDs) // 正确
(2)使用 Any
或泛型辅助函数(Go 1.18+)
func ToAnySlice[T any](s []T) []any {
result := make([]any, len(s))
for i, v := range s {
result[i] = v
}
return result
}
// 使用方式
ids := []int{1, 2, 3}
db.Where("id IN (?)", ToAnySlice(ids)) // 正确
(3)直接使用 []any
ids := []any{1, 2, 3} // 直接使用 []any
db.Where("id IN (?)", ids) // 正确
(4)直接使用Query
db.Query("select * from xxxx where id IN (1, 2, 3)")
db.Query("select * from xxxx where id IN (select id from xxxx group by id having count(*) > 1) order by id")
五、go切片扩容
// go 1.18 src/runtime/slice.go:178
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// ……
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
go语言中,切片扩容时,将使用growslice函数,代码capmem = roundupsize(uintptr(newcap) * ptrSize) newcap = int(capmem / ptrSize)的核心任务是:计算为了满足新容量需求所需申请的内存大小,并根据内存分配器的规则进行对齐和调整,最后计算出该内存大小所能承载的真正元素容量。
Go语言的内存分配器并不是你要多少字节它就给你精确分配多少字节。为了高效地减少内存碎片和简化管理,分配器定义了一系列预定义的大小等级(size class)。当你申请一定大小的内存时,分配器会向上取整到最近的一个预定义等级的大小。
例如,你可能只想申请10字节,但分配器可能实际分配给你16字节的块,因为16是它管理的最小单位之一。
capmem = roundupsize(uintptr(newcap) * ptrSize):
这行代码的作用是:计算需要向内存分配器申请的实际内存字节数。
newcap
: 是之前逻辑计算出的期望的新切片的元素个数。ptrSize
: 是切片中每个元素的大小(以字节为单位)。对于[]int64
,ptrSize
是8;对于[]byte
,ptrSize
是1。uintptr(newcap) * ptrSize
: 计算出存储newcap
个元素所需要的理论字节数。比如,期望容量newcap
是5,每个元素是int64
(8字节),那么理论需要5 * 8 = 40
字节。roundupsize()
: 这是一个运行时函数,它接收一个理论字节数,并返回Go内存分配器实际会分配的内存大小。这个大小会向上取整到最近的一个预定义的大小等级。
所以,capmem
变量存储的是实际要分配的内存字节数。
newcap = int(capmem / ptrSize)
这行代码的作用是:根据实际分配到的内存大小,反推切片最终的实际容量。
capmem / ptrSize
: 用实际分配到的内存总字节数,除以每个元素的大小,得到这块内存真正能够容纳的元素个数。newcap = int(...)
: 将计算结果重新赋值给newcap
。这意味着,之前计算出的期望容量newcap
被覆盖了。现在newcap
代表的是切片扩容后的真实容量,这个值可能会比之前计算的期望值更大,因为分配器多分配了一些内存。
最终,切片会以这个新的newcap
作为其底层数组的容量进行扩容。
匹配内存分配器:为了高效管理内存,分配器只能以特定大小的块来分配内存。直接申请任意大小的内存是低效且容易产生碎片的。
避免多次分配:通过一次分配稍多一点的内存,可以减少后续再次扩容的次数,从而提高性能(用空间换时间)。
内存对齐:roundupsize
也会确保分配的内存是适当对齐的,这有利于CPU高效访问内存。
六、go语言切片slice作为参数
Slice的结构:Slice本身是一个结构体(运行时表示),包含三个字段:
array
:一个指向底层数组的指针。
len
:当前切片的长度。
cap
:当前切片的容量。值传递:这是Go语言唯一参数传递方式。当slice作为函数参数时,这个结构体(包含指针、len、cap)会被完整地复制一份到函数内部,即便是传递slice的指针,也是值传递,slice指针作为形参时,会将地址拷贝一份传递给函数,函数内部会生成一个新的变量,但他们所存储的地址是一样的。
两种不同的“修改”:
修改元数据(不会影响原slice):在函数内修改拷贝而来的结构体中的len
或cap
,例如使用slice = slice[:2]
,这些修改只作用于副本,函数外原slice的len和cap保持不变。
修改底层数据(会影响原slice):通过副本中的array
指针去修改它指向的数组元素,例如slice[i] = newValue
或append
操作覆盖了已有元素。因为原slice和副本中的array
指针指向的是同一个数组,所以这些修改对两者都可见。Append操作的特殊情况:
Append操作可能触发两种行为:扩容并迁移:如果容量不足,
append
会申请新的、更大的底层数组,将老数据复制过去,再添加新元素。这时,副本的array
指针被更新为指向新数组。这个操作只发生在副本上,原slice的array
指针仍然指向老数组,因此两者从此分道扬镳,互不影响。原地修改:如果容量足够,
append
只是在底层数组的空闲位置添加新元素,并修改副本的len
。这时底层数组的数据变了(原slice可见),但原slice的len
没变(因为修改的是副本的len
)。
传Slice指针的作用:如果传slice的指针(
*[]int
),那么在函数内解引用后,可以直接修改调用者那个slice结构体本身的所有字段(array
,len
,cap
)。这意味着函数内的append
操作如果导致扩容,新的array
指针、len
和cap
会被写回调用者的原slice变量。在调用者看来,原slice变量被彻底改变了。
package main
import "fmt"
// modifySlice 接收一个slice的副本
func modifySlice(s []int) {
fmt.Printf("2. Inside modifySlice - s: %v, len: %d, cap: %d, ptr: %p\n", s, len(s), cap(s), &s[0])
// 情况A:通过指针修改底层数组 - 原slice可见
s[0] = 100
fmt.Printf("3. After modifying s[0] - s: %v\n", s)
// 情况B:修改副本的元数据(len/cap) - 原slice不可见
s = s[:2] // 缩短长度,这只修改了副本
fmt.Printf("4. After shortening s - s: %v, len: %d, cap: %d\n", s, len(s), cap(s))
// 情况C:append(未超cap)- 修改底层数组,但只修改副本的len
s = append(s, 200) // 追加元素,未扩容。覆盖了底层数组index=2的位置
fmt.Printf("5. After appending 200 (no grow) - s: %v, len: %d, cap: %d, ptr: %p\n", s, len(s), cap(s), &s[0])
// 情况D:append(触发扩容)- 副本指向新数组,与原slice彻底分离
s = append(s, 300, 400, 500) // 追加多个元素,触发扩容
s[0] = 999 // 修改新数组的元素,与原数组无关
fmt.Printf("6. After appending and growing - s: %v, len: %d, cap: %d, ptr: %p\n", s, len(s), cap(s), &s[0])
}
func main() {
originalSlice := make([]int, 3, 5) // len=3, cap=5
originalSlice[0] = 1
originalSlice[1] = 2
originalSlice[2] = 3
fmt.Printf("1. Original slice - s: %v, len: %d, cap: %d, ptr: %p\n", originalSlice, len(originalSlice), cap(originalSlice), &originalSlice[0])
modifySlice(originalSlice)
fmt.Printf("7. Back in main - s: %v, len: %d, cap: %d, ptr: %p\n", originalSlice, len(originalSlice), cap(originalSlice), &originalSlice[0])
// 注意观察:
// - index=0的值曾被改为100和999,但最终是100。说明情况A的修改共享,情况D的修改不共享。
// - index=2的值是200,这是情况C中append操作的成果,因为当时还未扩容,共享底层数组。
// - len和cap仍然是3和5,说明函数内对元数据的修改(情况B、C、D)都只作用于副本。
// - 数组指针始终未变(除非在main中扩容),说明情况D中的扩容只改变了副本的指针。
}
1. Original slice - s: [1 2 3], len: 3, cap: 5, ptr: 0x140000c2000
2. Inside modifySlice - s: [1 2 3], len: 3, cap: 5, ptr: 0x140000c2000 # 指针相同,共享数组
3. After modifying s[0] - s: [100 2 3] # 修改共享数组,原slice也会变
4. After shortening s - s: [100 2], len: 2, cap: 5 # 只改了副本的len
5. After appending 200 (no grow) - s: [100 2 200], len: 3, cap: 5, ptr: 0x140000c2000 # 未扩容,修改了共享数组index=2的位置,副本len恢复为3
6. After appending and growing - s: [999 2 200 300 400 500], len: 6, cap: 10, ptr: 0x140000c8000 # 扩容了,副本指向新数组,并修改了新数组
7. Back in main - s: [100 2 200], len: 3, cap: 5, ptr: 0x140000c2000 # 原slice看到的是共享数组的最后状态:100, 2, 200。对元数据和新数组的修改都不可见。