
go 所有类型均按值传递,但 map、slice、channel、func 和 string 等内置类型在底层封装了指针,因此函数内对其内容的修改(如赋值、追加)会反映到原始变量上;而普通结构体或基础类型则需显式使用指针接收器或参数才能实现外部可见的修改。
在 Go 中,“按值传递”(pass by value)是核心原则:每次赋值(u := t)或函数调用(f(t))都会复制该值的一份副本。但关键在于——“值”的含义取决于类型本身。对于 map[K]V,其底层是一个指向运行时 hmap 结构体的指针(即 *hmap),因此 map 类型的变量实际存储的是一个指针值;复制 map 变量,只是复制了这个指针,而非整个哈希表数据。这正是 map 能在值接收器方法中修改原始映射内容的根本原因。
以下代码清晰展示了这一行为:
type test1 map[string]int
func (t test1) DoSomething() {
t["yay"] = 1 // ✅ 修改生效:t 是指向同一底层 hmap 的副本指针
}
func main() {
t1 := test1{}
u1 := t1 // 复制 map 值(即复制指针)
u1.DoSomething()
fmt.Println("u1", u1) // map[yay:1]
fmt.Println("t1", t1) // map[yay:1] ← 原始变量也被修改!
}相比之下,[]int(切片)虽也包含指针(指向底层数组),但其长度与容量是独立字段。当在函数中执行 *t = append(*t, 1) 时,若触发扩容,底层数组地址可能变更,此时必须通过指针解引用才能更新调用方持有的切片头信息:
type test2 []int
func (t *test2) DoSomething() {
*t = append(*t, 1) // ✅ 必须用指针:append 可能返回新切片头
}而 struct 完全由字段值组成,无隐含指针。要修改其字段,必须使用指针接收器:
type test3 struct{ a string; b int }
func (t *test3) DoSomething() { // ❌ 若去掉 *,修改仅作用于副本
t.a, t.b = "aaa", 123
}? 重要澄清:Go 没有真正意义上的“引用传递”,也不支持 C++ 风格的 &T 参数引用语法。所谓“可修改”,本质是某些类型(map/slice/chan/func/string)的值本身携带指针语义。它们是 Go 运行时精心设计的“引用类型抽象”——既保持值传递的一致性,又避免用户手动管理指针的复杂性。
✅ 最佳实践总结:
- 对 map、chan、func:值接收器/参数即可安全修改其内容(增删键、发送接收、调用);
- 对 []T:若仅修改元素(s[i] = x),值接收器足够;若需改变长度(append)、扩容或重分配,则必须用指针;
- 对 struct 或基础类型:需显式使用指针接收器/参数,否则所有修改仅限于副本;
- 永远牢记:Go 传递的是值,而该值是否“指向共享状态”,由类型底层实现决定。
这种设计平衡了安全性与效率,也是理解 Go 内存模型的关键入口。









