golang中的unsafe包详解

本文探讨了Go语言中的unsafe包,讲述了如何在必要时进行类型不安全操作,如指针转换、系统调用和内存布局一致性。通过实例和规则说明了如何谨慎使用,避免内存安全问题,特别是涉及uintptr和Pointer的转换技巧及潜在风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从golang的定义来看,unsafe 是类型安全的操作。顾名思义,它应该非常谨慎地使用; unsafe可能很危险,但也可能非常有用。例如,当使用系统调用和Go结构必须具有与C结构相同的内存布局时,您可能别无选择,只能使用unsafe。关于指针操作,在unsafe包官方定义里有四个描述:

  1. 任何类型的指针都可以被转化为Pointer
  2. Pointer可以被转化为任何类型的指针
  3. uintptr可以被转化为Pointer
  4. Pointer可以被转化为uintptr

额外在加上一个规则:指向不同类型数据的指针,是无法直接相互转换的,必须借助unsafe.Pointer(类似于C的 void指针)代理一下再转换也就是利用上述的1,2规则。
举例:

func Float64bits(f float64) uint64 {
  // 无法直接转换,报错:Connot convert expression of type *float64 to type *uint64
  // return *(*uint64)(&f)   

 // 先把*float64 转成 Pointer(描述1),再把Pointer转成*uint64(描述2)
  return *(*uint64)(unsafe.Pointer(&f)) 
}

二、unsafe的定义:

整体代码比较简单,2个类型定义和3个uintptr的返回函数

package unsafe
//ArbitraryType仅用于文档目的,实际上并不是unsafe包的一部分,它表示任意Go表达式的类型。
type ArbitraryType int
//任意类型的指针,类似于C的*void
type Pointer *ArbitraryType
//确定结构在内存中占用的确切大小
func Sizeof(x ArbitraryType) uintptr
//返回结构体中某个field的偏移量
func Offsetof(x ArbitraryType) uintptr
//返回结构体中某个field的对其值(字节对齐的原因)
func Alignof(x ArbitraryType) uintptr
看一个栗子:
package main

import (
    "fmt"
    "unsafe"
)

type Human struct {
    sex  bool
    age  uint8
    min  int
    name string
}

func main() {
    h := Human{
        true,
        30,
        1,
        "hello",
    }
    i := unsafe.Sizeof(h)
    j := unsafe.Alignof(h.age
)
    k := unsafe.Offsetof(h.name)
    fmt.Println(i, j, k)
    fmt.Printf("%p\n", &h)
    var p unsafe.Pointer
    p = unsafe.Pointer(&h)
    fmt.Println(p)
}

//输出
//32 1 16
//0xc00000a080
//0xc00000a080
// 32:string 占16字节,所以16+16 =32;1 是因为age前是bool,占用1个字节;8是name的偏移是int 占8个字
节

三、Pointer使用

前面已经说了,pointer是任意类型的指针,可以指向任意类型数据。参照Float64bits的转换和上述栗子的unsafe.Pointer(&h),所以主要用于转换各种类型

四、uintptr

在golang中uintptr的定义是 type uintptr uintptr uintptr是golang的内置类型,是能存储指针的整型

  1. 根据描述3,一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)
  2. 这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。
  3. 许多将unsafe.Pointer指针转为uintptr,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:
package main

import (
    "fmt"
    "unsafe"
)

func main() {

    var x struct {
        a bool
        b int16
        c []int
    }

    /**
    unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
    */

    /**
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    指针的运算
    */
    // 和 pb := &x.b 等价
    pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
    *pb = 42
    fmt.Println(x.b) // "42"
}

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(注:这是真正可以体会unsafe包为何不安全的例子)。

下面段代码是错误的:

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

产生错误的原因很微妙。

有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存改变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动是对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

<think>嗯,用户想了解Golang中不同数据类型的定义以及它们在内存中的占用空间。首先,我需要回忆Golang的基本数据类型,比如整数、浮点数、布尔、字符串这些,还有复合类型如数组、切片、结构体、map和指针等。接下来,我得确定每种类型在内存中的大小,可能涉及到不同平台下的差异,比如int在32位和64位系统中的区别。 记得之前看过一些资料,比如引用[3]中提到,英文字符串每个字符占1字节,中文每个字符占3字节,这可能和UTF-8编码有关。字符串类型除了字符数据,还有指向底层数组的指针和长度信息,所以总大小应该是固定的头部加上字符数据本身的大小。比如,一个字符串变量可能占用16字节(64位系统)的固定部分,再加上字符长度乘以每个字符的字节数。 基本类型方面,像int8是1字节,int16是2字节,int32和rune是4字节,int64是8字节。uint同理。浮点数float32和float64分别占4和8字节。布尔类型bool是1字节,虽然理论上只需要1位,但内存对齐会让它占用1字节。指针的大小应该和系统架构相关,32位系统4字节,64位系统8字节。 复合类型的话,数组是连续存储的,所以总大小是元素类型大小乘以元素数量。比如[3]int32在64位系统下,每个int32是4字节,3个就是12字节。结构体需要考虑内存对齐,比如struct { a bool; b int32 },bool占1字节,可能需要填充3字节来让int32对齐到4字节,所以总大小是8字节。 切片的话,引用[1]提到切片由指针、长度和容量组成,每个部分在64位系统下是8字节,所以总共有24字节。map和通道的底层实现比较复杂,可能是指针或者引用类型,所以它们本身占用的空间可能是一个指针的大小,比如8字节,但实际数据存储在堆里,需要额外内存管理。 还有接口类型,比如interface{},含类型指针和数据指针,各占8字节,所以总共有16字节。空结构体struct{}不占空间,常用于占位符。 需要注意不同平台下的差异,比如int和uint在32位和64位系统下的大小不同。此外,内存对齐对结构体和数组的影响,可能会增加填充字节,导致实际占用空间比预期大。 用户可能还想知道如何优化内存使用,比如选择合适的数据类型,避免不必要的内存浪费。比如,如果知道数值范围较小,使用int8而不是int。结构体字段的顺序调整可以减少填充,节省空间。对于字符串,拼接操作可能产生临时变量,需要注意。 最后,用户的问题可能涉及到性能优化,所以需要结合内存占用和算法选择,比如引用[2]提到的堆和散列表的应用场景。需要确保提供的信息准确,并引用相关参考资料来支持回答。</think>### Golang数据类型内存占用详解 Golang的数据类型内存占用与其底层存储结构直接相关,以下是关键分类及内存占用分析: --- #### 一、基本数据类型 1. **整型** - `int8/uint8`(byte):$1$字节 - `int16/uint16`:$2$字节 - `int32`(rune)/`uint32`:$4$字节 - `int64/uint64`:$8$字节 - `int/uint`:32位系统$4$字节,64位系统$8$字节[^1] 2. **浮点型** - `float32`:$4$字节(IEEE-754单精度) - `float64`:$8$字节(IEEE-754双精度) 3. **布尔型** - `bool`:$1$字节(最小寻址单位限制) 4. **指针** - 32位系统$4$字节,64位系统$8$字节 --- #### 二、复合数据类型 1. **字符串(string)** ```go type string struct { ptr *byte len int } ``` - 固定占用$16$字节(64位系统) - 数据部分:英文$1$字节/字符,中文$3$字节/字符(UTF-8编码)[^3] 2. **数组** - 内存连续分配 - 示例:`[3]int32`占用$3 \times 4 = 12$字节 3. **切片(slice)** ```go type slice struct { ptr *T len int cap int } ``` - 固定占用$24$字节(64位系统)[^1] 4. **结构体(struct)** ```go type Example struct { a bool // 1字节 b int32 // 4字节 } // 总占用8字节(含3字节填充) ``` - 内存对齐规则可能导致填充字节 --- #### 三、引用类型 1. **map** - 底层哈希表实现 - 变量本身占用$8$字节(指针) 2. **通道(chan)** - 底层环形队列 - 变量本身占用$8$字节(指针) --- #### 四、特殊类型 1. **接口(interface)** ```go type iface struct { tab *itab data unsafe.Pointer } ``` - 固定占用$16$字节(64位系统) 2. **空结构体** - `struct{}`:$0$字节(用于信号传递) --- ### 内存优化技巧 1. 数值类型选择:根据范围选择`int8/int16` 2. 结构体字段排序:将同类型字段相邻存放减少填充 3. 字符串处理:避免频繁拼接(产生临时对象) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fish_study_csdn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值