0

0

Golang中哪些内置类型本质是指针类型 分析map/channel/function的内部实现

P粉602998670

P粉602998670

发布时间:2025-07-30 12:33:01

|

984人浏览过

|

来源于php中文网

原创

go语言中map、channel和function是引用类型的原因在于它们底层实现均通过指针进行数据共享。1. map本质是指向runtime.hmap结构的指针,赋值或传递时复制的是该指针,操作直接影响原始数据;2. channel底层是指向runtime.hchan结构的指针,包含缓冲区、互斥锁等并发控制信息,确保多goroutine安全通信;3. function作为一等公民,普通函数指向代码入口点,闭包则通过runtime.funcval结构体保存代码入口及捕获变量,实现环境隔离与状态保持。这些机制共同保证了高效的数据访问与并发安全性。

Golang中哪些内置类型本质是指针类型 分析map/channel/function的内部实现

在Go语言中,mapchannelfunction(或者说 func 类型的值)在本质上都是引用类型。它们在作为参数传递或赋值时,传递的并非底层数据的完整副本,而是指向其内部数据结构的一个指针。这意味着对这些类型值的任何修改,都会直接作用于原始数据,而非一个拷贝。理解这一点,对于编写高效且行为可预测的Go程序至关重要。

Golang中哪些内置类型本质是指针类型 分析map/channel/function的内部实现

解决方案

要深入理解mapchannelfunction为何是引用类型,我们需要从它们在Go运行时中的底层实现机制来分析。

Golang中哪些内置类型本质是指针类型 分析map/channel/function的内部实现

Map的本质:指向hmap结构的指针 在Go中,map类型变量的底层实际上是一个指向 runtime.hmap 结构体的指针。当你通过 make(map[keyType]valueType) 创建一个map时,Go运行时会在堆上分配一块内存来存储这个 hmap 结构,并返回一个指向它的指针。因此,当你把一个map赋值给另一个变量,或者作为函数参数传递时,你传递的都是这个 hmap 结构体的指针副本。这意味着,即便在函数内部对map进行了增、删、改操作,也都是直接修改了同一个 hmap 结构,所以这些改变在函数外部是可见的。一个 nil 的map表示它还没有指向任何 hmap 结构,尝试对其进行读写操作会引发运行时panic。

Channel的本质:指向hchan结构的指针 与map类似,channel类型变量的底层是一个指向 runtime.hchan 结构体的指针。这个 hchan 结构包含了channel的缓冲区、发送/接收队列、以及用于并发控制的互斥锁等信息。通过 make(chan T, capacity) 创建一个channel时,Go运行时同样会在堆上分配并初始化一个 hchan 结构,并返回一个指向它的指针。因此,channel的传递和map一样,传递的是其底层 hchan 结构的引用。对channel的发送、接收或关闭操作,都是直接操作这个共享的 hchan 结构,从而实现了goroutine之间的安全通信。一个 nil 的channel,其发送和接收操作都会永久阻塞。

Golang中哪些内置类型本质是指针类型 分析map/channel/function的内部实现

Function的本质:代码入口点或闭包结构 Go中的function,或者说func类型的值,也是一种引用类型。对于普通的、非闭包的函数,func值本质上是一个指向该函数代码入口点的指针。当你把一个函数赋值给变量或作为参数传递时,你传递的就是这个入口点的地址。 而对于闭包(closure),情况稍微复杂一些。一个闭包不仅仅包含函数的代码入口点,它还需要捕获其定义时所在环境中的外部变量。在Go运行时中,一个闭包的func值实际上是一个指向 runtime.funcval 结构体的指针。这个 funcval 结构包含两个主要部分:一个是函数代码的入口点(fn字段),另一个是指向被捕获变量集合的指针(通常是一个指向栈帧或堆上分配的捕获变量区域的指针)。因此,当你传递一个闭包时,你传递的是这个 funcval 结构体的引用,它包含了执行代码和访问其捕获变量所需的所有信息。

map的内部实现:为什么它是引用类型?

说白了,map在Go里头就是个哈希表的封装。当你声明一个 var m map[string]int 但不 make 它的时候,m 的值是 nil。这就像你有一个指向某个东西的引用,但这个引用目前指向的是“空”。当你执行 m = make(map[string]int),Go运行时会在内存里头找块地方,分配一个 runtime.hmap 结构体,然后把 m 这个变量指向它。这个 hmap 结构里包含了哈希表的元数据,比如当前元素数量、哈希桶(buckets)的指针、以及一些用于扩容和并发安全的字段。

立即学习go语言免费学习笔记(深入)”;

所以,当你把 m 传给一个函数,比如 func modifyMap(data map[string]int),实际上传递的是 m 所指向的那个 hmap 结构体的内存地址。函数内部对 data 的任何操作,比如 data["new_key"] = 10,都是直接通过这个地址去修改了原始的 hmap 结构。这跟C语言里传指针的效果是一样的,只是Go在语法层面做了更高层次的抽象,让你感觉不到指针的存在,但其底层行为就是如此。

举个例子:

package main

import "fmt"

func addElement(m map[string]int) {
    fmt.Printf("函数内部 map 地址: %p\n", m) // 打印的是hmap的地址
    m["apple"] = 5
    m["banana"] = 10
}

func main() {
    myMap := make(map[string]int)
    fmt.Printf("主函数 map 地址: %p\n", myMap) // 和函数内部地址一致
    myMap["orange"] = 3

    fmt.Println("修改前:", myMap) // map[orange:3]
    addElement(myMap)
    fmt.Println("修改后:", myMap) // map[apple:5 banana:10 orange:3]
}

从输出的地址可以看出,myMapm 变量指向的是同一个底层 hmap 结构。这就是为什么对map的修改会影响到原变量。这其实也是Go设计哲学的一部分:对于复杂的数据结构,避免不必要的深拷贝,从而提高性能。当然,这也意味着你需要清楚其引用特性,避免意外的副作用。

channel的内部机制:并发安全的秘密与指针特性

channel是Go并发编程的核心原语之一,它的实现同样依赖于指针。一个channel变量的底层,是指向 runtime.hchan 结构体的指针。这个 hchan 结构包含了实现并发通信所需的所有组件:

  • qcount: 当前channel中排队的元素数量。
  • dataqsiz: channel的缓冲区容量。
  • buf: 指向实际存储元素的环形缓冲区的指针(如果channel是带缓冲的)。
  • elemsize: channel中每个元素的大小。
  • closed: 一个标志位,指示channel是否已关闭。
  • sendx/recvx: 环形缓冲区中发送/接收的下一个位置索引。
  • recvq/sendq: 等待接收/发送的goroutine队列(sudog列表)。
  • lock: 一个互斥锁,用于保护 hchan 结构体的所有字段,确保在并发访问时的安全性。

当你通过 ch := make(chan int, 5) 创建一个channel时,Go运行时会在堆上分配并初始化一个 hchan 结构,然后将这个结构的地址赋给 ch 变量。当你把 ch 传递给另一个goroutine或函数时,同样传递的是这个 hchan 结构的地址。

因此,无论是在哪个goroutine中对这个 ch 进行发送 (ch )、接收 () 或关闭 (close(ch)) 操作,都是直接操作同一个 hchan 结构。hchan 内部的 lock 字段确保了这些操作在并发环境下的原子性和一致性。这就是channel能够作为安全通信管道的关键所在。

TTSMaker
TTSMaker

TTSMaker是一个免费的文本转语音工具,提供语音生成服务,支持多种语言。

下载

考虑这个场景:

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    fmt.Printf("Worker %d 接收到 channel 地址: %p\n", id, ch)
    for val := range ch {
        fmt.Printf("Worker %d 接收到: %d\n", id, val)
    }
    fmt.Printf("Worker %d channel 已关闭。\n", id)
}

func main() {
    myChannel := make(chan int, 3)
    fmt.Printf("主 goroutine channel 地址: %p\n", myChannel)

    go worker(1, myChannel)
    go worker(2, myChannel)

    time.Sleep(100 * time.Millisecond) // 等待 worker 启动

    for i := 0; i < 5; i++ {
        myChannel <- i
        fmt.Printf("主 goroutine 发送: %d\n", i)
    }

    close(myChannel) // 关闭 channel
    time.Sleep(1 * time.Second) // 等待 worker 处理完并退出
}

你会发现,worker 函数内部接收到的 ch 变量,其底层 hchan 的地址与 main 函数中的 myChannel 是完全一致的。所有goroutine都在操作同一个共享的channel实例,而其内部的锁机制则保证了数据的一致性,避免了竞态条件。

function作为一等公民:闭包与函数指针的本质

在Go语言中,函数被视为“一等公民”,这意味着它们可以像其他值一样被赋值给变量、作为参数传递给其他函数,或者作为另一个函数的返回值。这种特性背后的实现,也离不开指针。

对于一个普通的、不捕获任何外部变量的函数,比如:

func add(a, b int) int {
    return a + b
}

当你将 add 赋值给一个变量 var op func(int, int) int = add,或者将其作为参数传递时,Go实际上是传递了一个指向 add 函数代码在内存中起始位置的指针。这个指针就是函数的“入口点”。

然而,当涉及到闭包时,事情就变得更有趣了。闭包是一种特殊的函数,它“记住”了它被创建时的环境,即使该环境已经不再活跃(例如,创建闭包的函数已经返回),闭包仍然可以访问和修改那些被它捕获的外部变量。

在Go的运行时中,一个闭包的func值实际上是一个指向 runtime.funcval 结构体的指针。这个 funcval 结构大致可以理解为:

type funcval struct {
    fn uintptr // 指向函数代码的入口点
    // 后面跟着的是被捕获的变量(或者指向这些变量的指针)
    // 这些变量构成了闭包的“环境”
}

当一个闭包被创建时,如果它捕获了外部变量,Go运行时会确保这些变量(或者它们的副本/指针)被存储在堆上,或者以某种方式在闭包的生命周期内保持可访问。funcval 结构中的 fn 字段指向闭包的代码,而 fn 之后紧跟着的内存区域则存储了对这些被捕获变量的引用。因此,当你传递一个闭包时,你传递的是这个 funcval 结构体的指针,它包含了执行闭包所需的所有信息:代码在哪里,以及它需要访问的外部变量在哪里。

来看一个闭包的例子:

package main

import "fmt"

func makeCounter() func() int {
    count := 0 // 被闭包捕获的外部变量
    return func() int {
        count++
        return count
    }
}

func main() {
    counter1 := makeCounter()
    counter2 := makeCounter() // 创建一个新的闭包实例,拥有独立的 count 变量

    fmt.Println("Counter 1:", counter1()) // 1
    fmt.Println("Counter 1:", counter1()) // 2
    fmt.Println("Counter 2:", counter2()) // 1
    fmt.Println("Counter 1:", counter1()) // 3
}

在这个例子中,makeCounter 返回的匿名函数是一个闭包,它捕获了 makeCounter 函数栈帧上的 count 变量。当 makeCounter 返回时,count 变量的生命周期并没有结束,因为它被闭包引用了。Go运行时会把 count 提升到堆上,或者以其他方式保证其在闭包存活期间的可见性。counter1counter2 分别是两个独立的 funcval 实例,它们各自指向一个独立的 count 变量,所以它们的计数是互不影响的。这就是函数作为一等公民,尤其是闭包,其底层指针特性的体现。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

379

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

608

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

348

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

255

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

583

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

519

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

631

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

595

2023.09.22

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
golang socket 编程
golang socket 编程

共2课时 | 0.1万人学习

nginx浅谈
nginx浅谈

共15课时 | 0.8万人学习

golang和swoole核心底层分析
golang和swoole核心底层分析

共3课时 | 0.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号