go 中各种类型占用空间大小

在 Go 语言中,了解不同数据类型占用的内存空间对于性能优化和内存管理至关重要。以下是一些常见数据类型在Go语言中的内存占用情况:

基本数据类型

  • 布尔类型,bool:占用 1 字节。

  • 字节类型,byte:byte 是 uint8 的内置别名。 占用 1 个字节。

  • 宽字符类型,rune:rune 是 int32 的内置别名,占用 4 字节。

  • 整数类型,int、uint:在 32 位系统上占用 4 字节,在 64 位系统上占用 8 字节。

  • 8 位整型,int8、uint8:占用 1 字节,取值范围分别是[-128 -> 127][0 -> 255]

  • 16 位整型,int16、uint16:占用 2 字节,取值范围分别是[-32768 -> 32767][0 -> 65535]

  • 32 位整型, int32、uint32:占用 4 字节,取值范围分别是-21亿到21亿和0到42亿。

  • 64 位整型,int64、uint64:占用 8 字节。

  • 浮点数类型,float32 占用 4 字节;float64 占用 8 字节。

  • 复数类型,complex64、complex128:分别占用 8 字节和 16 字节。

  • 指针类型,uintptr:用于存储指针的整数,32 位系统上占用 4 字节,64 位系统上占用 8 字节。

以上基本数据类型的大小都是固定的,因此就不多说什么了,可以使用unsafe.Sizeof()函数去验证一下。

package main

import (
    "fmt"
    "unsafe"
)

func main() {

    // 基本类型大小
    fmt.Println("\nSize of basic types:")
    fmt.Printf("bool:        %d byte(s)\n", unsafe.Sizeof(bool(false)))
    fmt.Printf("byte:        %d byte(s)\n", unsafe.Sizeof(byte(0)))
    fmt.Printf("rune:        %d byte(s)\n", unsafe.Sizeof(rune(0)))
    fmt.Printf("int:         %d byte(s) (architecture-dependent)\n", unsafe.Sizeof(int(0)))
    fmt.Printf("uint:        %d byte(s) (architecture-dependent)\n", unsafe.Sizeof(uint(0)))
    fmt.Printf("int8:        %d byte(s)\n", unsafe.Sizeof(int8(0)))
    fmt.Printf("uint8:       %d byte(s)\n", unsafe.Sizeof(uint8(0)))
    fmt.Printf("int16:       %d byte(s)\n", unsafe.Sizeof(int16(0)))
    fmt.Printf("uint16:      %d byte(s)\n", unsafe.Sizeof(uint16(0)))
    fmt.Printf("int32:       %d byte(s)\n", unsafe.Sizeof(int32(0)))
    fmt.Printf("uint32:      %d byte(s)\n", unsafe.Sizeof(uint32(0)))
    fmt.Printf("int64:       %d byte(s)\n", unsafe.Sizeof(int64(0)))
    fmt.Printf("uint64:      %d byte(s)\n", unsafe.Sizeof(uint64(0)))
    fmt.Printf("float32:     %d byte(s)\n", unsafe.Sizeof(float32(0)))
    fmt.Printf("float64:     %d byte(s)\n", unsafe.Sizeof(float64(0)))
    fmt.Printf("complex64:   %d byte(s)\n", unsafe.Sizeof(complex64(0)))
    fmt.Printf("complex128:  %d byte(s)\n", unsafe.Sizeof(complex128(0)))
    fmt.Printf("uintptr:     %d byte(s) (architecture-dependent)\n", unsafe.Sizeof(uintptr(0)))
}

复合数据类型

字符串

Go 语言的字符串是一个不可变的字节序列。在 Go 的源代码中,字符串是以如下类似的方式定义的(简化版):

type stringStruct struct {
    ptr *byte
    len int
}
  • ptr:指向存储实际字符数据的内存地址。
  • len:表示字符串的长度,即字符的数量。

因此无论一个字符串变量多长,其本身都只占用 16 字节。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s1 string
    var s2 string = "123456789"
    fmt.Println("String s1 size:", unsafe.Sizeof(s1)) // String s1 size: 16
    fmt.Println("String s2 size:", unsafe.Sizeof(s2)) // String s2 size: 16
}

数组(Array)

数组是值类型,其大小取决于元素类型字节数和数组长度的乘积。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a1 [5]int
    var a2 = [3]string{"Red", "Blue", "Green"}
    fmt.Println("Array a1 size:", unsafe.Sizeof(a1)) // 40
    fmt.Println("Array a2 size:", unsafe.Sizeof(a2)) // 48
}

[5]int 数组的大小是5 * 8 = 40[3]string数组的大小是3 * 16 = 48

切片(Slice)

Slice 实际在 runtime 中的实现是一个结构体,其内部组成如下:

type slice struct { 
    array unsafe.Pointer 
    len int 
    cap int 
}

每一项都是占用 8 字节,所以总大小是 24 字节。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var s1 []int
    s2 := make([]string, 10, 20)
    fmt.Println("Slice s1 size:", unsafe.Sizeof(s1)) // 24
    fmt.Println("Slice s2 size:", unsafe.Sizeof(s2)) // 24
}

可以看到,无论 Slice 的实际大小是多大,Slice 的变量大小都是 24。

结构体(struct)

在 Go 中,结构体(struct)占用的内存大小取决于其字段的类型和排列顺序。Go 编译器会根据字段的类型大小和内存对齐要求自动调整字段的排列以优化内存使用。为了计算结构体实际占用的内存大小,一般需要考虑以下几个因素:

  1. 字段类型:每个字段的数据类型决定了它需要多少字节。
  2. 自然边界对齐填充:为了提高访问速度,编译器可能会在字段之间插入填充字节,使得每个字段都按照其类型的自然边界对齐。对于各种基本数据类型来说,它的变量的内存地址值必须是其类型本身大小的整数倍。
  3. 结构体本身的对齐:整个结构体也需要根据最宽的基本类型进行对齐。
package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    A byte
    B string
    C int64
}

func main() {
    var e Example
    fmt.Println("Struct e size:", unsafe.Sizeof(e)) // 32
}

Example 结构体中有三个字段:

  • A byte: 1 字节 (8 bits)

  • B string: 16 字节。

  • C int64: 8 字节。

正常来说,你可能会认为是1+16+8=25,但实际unsafe.Sizeof函数输出是32。为什么会是这样呢?实际上 Go 编译器在为结构体分配内存时根据最宽的基本类型对整个结构体进行对齐,并且每个字段也会按照其类型的自然边界对齐。对于Example,最宽的类型是int64string内部的指针,它们都需要 8 字节对齐。

计算过程:

  1. A byte: 需要 1 字节,但是为了使接下来的字段 B string 能够 8 字节对齐,编译器会在 A 后面插入 7 字节的填充,使得总大小变为 8 字节。

  2. B string: 由于 A 已经通过填充实现了 8 字节对齐,所以 B 可以直接放置,占用 16 字节。

  3. C int64: B 占用 16 字节后已经是 8 字节对齐了,因此 C 可以紧跟着 B 放置,占用 8 字节。

所以,Example 结构体的实际布局如下:A byte: 1 字节 + 7 字节填充 = 8 字节;B string: 16 字节;C int64: 8 字节;总计:8 + 16 + 8 = 32 字节。这就是为什么unsafe.Sizeof(e)返回的结果是 32 字节的原因了。

映射(Map)

map 是比较复杂的数据结构,实际存储的是哈希表,但对用户来说,他的变量其实就是一个指向这个哈希表的指针,占用 8 字节。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var m1 map[int]int
    m2 := make(map[string]string,10)
    m2["name"] = "zhangsan"
    fmt.Println(m1) // map[]
    fmt.Println("Map m1 size:", unsafe.Sizeof(m1)) // Map m1 size: 8
    fmt.Println(m2) // map[name:zhangsan]
    fmt.Println("Map m2 size:", unsafe.Sizeof(m2)) // Map m2 size: 8
}

map 的实际大小取决于存储的键值对数量和它们的大小,但 map 变量本身只是一个指针。

通道(channel)

同 map 一样,channel 也是一个复杂的数据结构,他对应的变量也是一个指针,占用 8 字节。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var ch chan int
    fmt.Println("Channel size:", unsafe.Sizeof(ch)) // 8
}
go
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!