Go语言学习(二)

发布于:2025-09-04 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、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 构建器(如 gormsqlxsquirrel 或其他 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: 是切片中每个元素的大小(以字节为单位)。对于[]int64ptrSize是8;对于[]byteptrSize是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作为参数

  1. Slice的结构:Slice本身是一个结构体(运行时表示),包含三个字段:

    array:一个指向底层数组的指针
    len:当前切片的长度。
    cap:当前切片的容量。
  2. 值传递:这是Go语言唯一参数传递方式。当slice作为函数参数时,这个结构体(包含指针、len、cap)会被完整地复制一份到函数内部,即便是传递slice的指针,也是值传递,slice指针作为形参时,会将地址拷贝一份传递给函数,函数内部会生成一个新的变量,但他们所存储的地址是一样的。

  3. 两种不同的“修改”

    修改元数据(不会影响原slice):在函数内修改拷贝而来的结构体中的lencap,例如使用slice = slice[:2],这些修改只作用于副本,函数外原slice的len和cap保持不变。
    修改底层数据(会影响原slice):通过副本中的array指针去修改它指向的数组元素,例如slice[i] = newValueappend操作覆盖了已有元素。因为原slice和副本中的array指针指向的是同一个数组,所以这些修改对两者都可见。
  4. Append操作的特殊情况

    Append操作可能触发两种行为:
    • 扩容并迁移:如果容量不足,append会申请新的、更大的底层数组,将老数据复制过去,再添加新元素。这时,副本的array指针被更新为指向新数组。这个操作只发生在副本上,原slice的array指针仍然指向老数组,因此两者从此分道扬镳,互不影响。

    • 原地修改:如果容量足够,append只是在底层数组的空闲位置添加新元素,并修改副本的len。这时底层数组的数据变了(原slice可见),但原slice的len没变(因为修改的是副本的len)。

  5. 传Slice指针的作用:如果传slice的指针(*[]int),那么在函数内解引用后,可以直接修改调用者那个slice结构体本身的所有字段(arraylencap)。这意味着函数内的append操作如果导致扩容,新的array指针、lencap会被写回调用者的原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。对元数据和新数组的修改都不可见。