Go语言学习(二)

一、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

Go 复制代码
// 验证中文
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 是一个用于确保某个函数只执行一次的同步原语。它通常用于初始化操作,比如初始化一个全局变量或执行一次性的设置。

Go 复制代码
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远程调试

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"

如果不是调试脚本,调试项目,

Go 复制代码
dlv debug --headless --listen=:8349 --api-version=2 -- -conf ./conf/app.toml 
如果不需要指定配置文件,可取消--及其后面的参数

如果有需要也可以调试正在运行的代码

Go 复制代码
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 复制代码
// 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结构体本身 的所有字段(array, len, cap)。这意味着函数内的append操作如果导致扩容,新的array指针、lencap会被写回调用者的原slice变量。在调用者看来,原slice变量被彻底改变了

Go 复制代码
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。对元数据和新数组的修改都不可见。
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意6 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码6 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习