Go语言第十章(指针)

值传递与引用类型

调用函数时,实参怎么传给形参?改形参会不会影响外面的变量吗?

Go 的答案比「值传递 / 引用传递」二分法更精确:Go 里永远是值传递------传参时会复制一份。但有些类型的「那一份」只是描述符(header),底层数据仍然共享,所以看起来就像「引用传递」。

先记住一句话

Go 只有值传递,没有 C++ 那种引用传递(&)。

区别不在于语法,而在于被复制的到底是什么 :复制整个 int,还是复制一个指向底层数据的「头」。

分类 常见类型 传参时复制什么 函数内改形参,外面会变吗
值类型 intboolfloatarraystruct(普通字段) 完整拷贝一份数据 不会(改的是副本)
引用类型 slicemapfuncinterface 复制 header,共享底层 (改的是同一份底层数据)
指针 *T 复制指针值(地址) (通过地址改原数据)

下面分开看。

值传递

值传递时,函数拿到的是实参的完整副本。形参和实参是两个变量,互不影响。

go 复制代码
package main

import "fmt"

func change(x int) {
	x = 20 // 改的是副本
}

func main() {
	x := 10
	change(x)
	fmt.Println(x) // 10,外面的 x 没变
}

数组也是值类型------传参会复制整个数组,开销比切片大,日常更常用切片:

go 复制代码
package main

import "fmt"

func change(arr [3]int) {
	arr[0] = 100
}

func main() {
	arr := [3]int{1, 2, 3}
	change(arr)
	fmt.Println(arr) // [1 2 3],原数组没变
}

引用类型有哪些

Go 没有官方术语「引用类型」,但社区通常把下面几类归为一组------它们的变量里藏着指向底层数据的指针

类型 说明
slice 切片,可变长序列
map 映射,键值对
func 函数值,指向同一段代码
interface 接口值,内部含类型与数据指针
chan 通道,用于 goroutine 通信(本节仅了解名字,详细用法见后续专题

下面用 slicemap 举例(入门阶段重点掌握这两个);funcinterface 会在函数、接口相关章节深入,这里知道「也属于引用类型」即可。

切片 slice

传参时复制的是 slice header(指针、长度、容量),底层数组共享。所以在函数里改元素,外面看得见:

go 复制代码
package main

import "fmt"

func change(s []string) {
	s[0] = "猫来"
}

func main() {
	s := []string{"牛来", "羊来", "码来"}
	change(s)
	fmt.Println(s) // [猫来 羊来 码来]
}

append 追加元素时,只要没触发扩容 (未超过 cap),同样共享底层数组:

go 复制代码
package main

import "fmt"

func appendOne(s []int) {
	s = append(s, 100) // 在共享的底层数组上追加
}

func main() {
	s := make([]int, 0, 4) // len=0, cap=4
	appendOne(s)
	fmt.Println(s, len(s)) // [] 0 ------ 注意:形参 s 的 len 变了,但 main 里的 s 仍是原来的 header
}

上面这个例子要仔细看:appendOneslen 变了,但 main 里的 s 还是原来的 header(len 仍为 0)。

改已有下标的元素 会生效;append 可能只改形参自己的 header ,外面不一定变长。若 append 触发扩容、分配了新数组,则完全与外面无关。

go 复制代码
package main

import "fmt"

func appendMany(s []int) {
	s = append(s, 1, 2, 3) // cap 不够时会新分配数组
}

func main() {
	s := make([]int, 0, 1)
	appendMany(s)
	fmt.Println(s, len(s)) // [] 0 ------ 扩容后外面完全不受影响
}

要点:s[i] 共享;append 是否影响外面,要看有没有扩容、以及是否把新 slice 返回给调用方。

映射 map

map 传参后,内外指向同一张哈希表。改值、删键、增键,外面都看得见:

go 复制代码
package main

import "fmt"

func change(m map[string]int) {
	m["语文"] = 800
}

func main() {
	m := map[string]int{
		"语文": 100,
		"数学": 99,
		"英语": 98,
	}
	change(m)
	fmt.Println(m) // map[语文:800 数学:99 英语:98]
}

调用 delete 等 API,同样共享底层:

go 复制代码
package main

import "fmt"

func change(m map[string]int) {
	m["语文"] = 800
	delete(m, "数学")
}

func main() {
	m := map[string]int{
		"语文": 100,
		"数学": 99,
		"英语": 98,
	}
	change(m)
	fmt.Println(m) // map[语文:800 英语:98]
}

通道 chan(了解即可)

chanslicemap 一样,传参后内外指向同一个通道。

通道用于 goroutine 之间收发数据,用法较多,本教程不展开 ;后续会有单独一篇。这里只要记住:它也是引用类型之一

结构体:值传递 vs 指针传递

结构体本身是值类型 。传 Person 会复制整个结构体,函数里改字段不影响外面:

go 复制代码
package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

func change(person Person) {
	person.Name = "张满月"
	person.Age = 91
	person.Email = "zmy@qq.com"
}

func main() {
	person := Person{
		Name:  "小满",
		Age:   25,
		Email: "1195566313@qq.com",
	}
	change(person)
	fmt.Println(person) // {小满 25 1195566313@qq.com},原结构体未变
}

若希望函数内修改能反映到外面,传指针 *Person

go 复制代码
package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

func change(person *Person) {
	person.Name = "张满月" // 语法糖,等价于 (*person).Name
	person.Age = 91
	person.Email = "zmy@qq.com"
}

func main() {
	person := Person{
		Name:  "小满",
		Age:   25,
		Email: "1195566313@qq.com",
	}
	change(&person)
	fmt.Println(person) // {张满月 91 zmy@qq.com}
}

补充说明:

  • *Person指针类型 ,不是「引用类型」表格里的那一类;传指针时复制的是地址,通过地址仍能改到原结构体。
  • 结构体里某字段写成 *string,表示该字段是指针,和「把整个人当成 *Person 传递」是不同层面的概念。入门阶段优先掌握 Person*Person 的区别 即可。

怎么选

场景 建议
基本类型、小结构体,函数只读不改 直接传值
需要在函数里改调用方的结构体 *Struct
一组可变长数据 slice,注意 append 与共享的规则
按键查找、统计 map
不确定要不要改外面 先想清楚「要不要共享」;要共享就指针或引用类型,不要共享就传值或返回新值

作业

  1. 值传递 :写 setZero(n int),在函数内把 n 设为 0。在 main 里传入 42,调用前后各打印一次,确认外面仍是 42
  2. 切片改元素 :写 firstToX(s []string),把第一个元素改成 "X"。在 main 里传入 []string{"A", "B", "C"},调用后打印切片,确认已改变。
  3. map 增删改 :写 updateScore(scores map[string]int, subject string, delta int),给某科加分;若科目不存在则新建。再写 removeSubject(scores map[string]int, subject string) 删除一科。在 main 里验证增、改、删后 map 的变化。
  4. 结构体值 vs 指针 :定义 Counter struct { Value int }。分别写 addByValue(c Counter)addByPointer(c *Counter),都让 Value + 1。在 main 里各调用一次,对比哪种方式能让外面的 Counter 真的加 1。
  5. 综合练习 :用 map[string][]int 表示「学生姓名 → 各科成绩列表」。写函数 addScore(scores map[string][]int, name string, score int) 追加一门成绩;写 fixFirst(scores map[string][]int, name string, newScore int) 把该生第一门课分数改成 newScore(若存在)。在 main 里多次调用后打印,体会 map 与 slice 嵌套时的共享行为。
相关推荐
高级程序源10 小时前
django大学生创新创业项目管理系统94923-计算机课程设计、毕业设计
javascript·vue.js·spring boot·后端·python·django·课程设计
Sam_Deep_Thinking10 小时前
new Thread()之后发生了什么?
java·后端·面试·程序员
步行cgn10 小时前
Spring util 命名空间详解
java·后端·spring
打工仔折腾 AI11 小时前
普通摄像头接入AI识别:绿联NAS部署Frigate监控实战
人工智能·后端·python·性能优化·ai agent 实战
万物智能13 小时前
SARADC模数转换—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
前端·后端
花间相见13 小时前
【后端开发|网络编程】—— 五种实时通信方案全解析:短轮询、长轮询、SSE、MQTT、WebSocket
后端·网络协议
坤岭13 小时前
LLM应用安全护栏实战
后端
程序猿阿越13 小时前
containerd如何创建Pod
后端·kubernetes·源码阅读
名字还没想好☜13 小时前
Python sqlite3 实战:事务提交、参数化防注入、WAL 并发与 row_factory 取字典
后端·python·编程语言