跳转至

数据结构

String

底层结构

runtime 包下:

type stringStruct struct {
    str unsafe.Pointer
    len int // 存的是字节数
}

字符串的操作包括 写 / 拼接 等都是通过拷贝来实现的

作为函数参数

当 string 作为函数参数时,传递的是底层结构体的副本。当对字符串进行拼接操作时,修改的是副本中 str 指针指向的内存地址:其指向凭借后新字符串的地址。函数外字符串不会受影响

类型转换

  • string(100) 这里的 100 被视为 rune,会被转换为 Unicode 编码的 "d" (rune 本质是 int32)
    • 如果想得到 "100",需要 strconv.Itoa(100) 或 fmt.Sprint(100)
    • 相反,如果想吧 string 类型的 s 的 s[0] 转换为 string 类型,就可以使用 var tmp = string(s[0])
  • 如果数字超过 Unicode 的编码范围,就会返回 �

与 []byte 互转

string 是不可变的,比如 s[0] = 'A' 会报错。可以通过 []byte 来实现修改:

s := "hello"
b := []byte(s)
b[0] = 'H'
s = string(b)

需要注意的是,这里发生了两次拷贝过程

``初始化

使用 `` 初始化字符串可以初始化跨行、内部带有 " " 的字符串而避免使用转义字符

json := `{"hello": "go", "name": ["xu"]}`

type TestStruct struct {
    Text  string   `json:"hello"`  // 映射 JSON 的 "hello" 字段
    Names []string `json:"name"`   // 映射 JSON 的 "name" 字段
}

var data TestStruct
err := json.Unmarshal([]byte(json), &data)

json.Unmarshal 大小写不敏感

转义问题

`` 是原始字符串转义的定界符

// 普通字符串(需要转义)
str1 := "\"nginx reload enabled\""  // 需要转义双引号

// 原始字符串(不需要转义)
str2 := `"nginx reload enabled"`    // 直接包含双引号

字符串拼接

追加写入的方式:

  • +
    • 注意这里如果要加 'a' 需要强转换 string('a')
  • append
  • strings.Builder (性能最优,分配内存时自动扩容)
  • bytes.Buffer
var builder strings.Builder

builder.WriteString("Hello")
builder.WriteByte(' ')
builder.WriteRune('世') // unicode
builder.WriteRune('界')
builder.Write([]byte("!"))

result := builder.String() // 复用内存
fmt.Println(result) // 输出: Hello 世界!

builder.Reset()

切片

切片是 go 中的一种类型,零值为 nil

底层结构

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

image-20250505222849496

image-20250505222909007

与数组的区别

数组作为一个整体性的结构存储(因为是定长的)

将数组转为 slice:arr[:] 即得到一个 slice 类型

拷贝方式

  • 使用赋值拷贝/函数传参时,拷贝的是 slice header,二者共享底层数组;但是当发生扩容时,新切片将指向新的数组
  • 使用内置的 copy(dst, src) 方法可以进行深拷贝,拿到一个完全独立的副本

作为参数传递

切片作为函数参数传递时,传入函数的是切片底层结构体的副本;

特别地,当传入 arr[1:] 这类切片的截取时,传入结构体的 array 指针指向了 arr[1] 所在的地址

  • 在函数内部设置 slice[0] = 1,会修改原切片(指针相同,相同地址处的值被修改了)
  • 在函数内因为 append 操作导致切片扩容,不会修改原切片:会导致底层结构体指针指向的地址重分配
  • 在函数内部进行 slice = slice[1:] ,不会修改原切片:底层结构体的副本中指针更新

扩容策略

1.20.4

首先得到新的长度:

  • 如果扩容的目标长度(oldLen + num(append)) > 2 * 旧容量,直接扩容为目标长度
  • 否则
    • 如果旧切片的容量 < 256,新容量扩容为旧容量的两倍
    • 否则,旧切片容量按照 1.25 倍递增,直到新容量 >= 目标长度

随后需要进行内存对齐,同时将旧的切片中的元素复制到新分配的内存中

例子

package main

import "fmt"

func main() {
    doAppend := func(s []int) {
        s = append(s, 1)
        printLenAndCap(s)
    }
    s := make([]int, 8, 8)
    doAppend(s[:4])
    printLenAndCap(s)
    doAppend(s)
    printLenAndCap(s)
}

func printLenAndCap(s []int) {
    fmt.Println(s)
    fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
}

输出结果

00001, len 5, cap 8
00001000, len 8, cap 8
000010001, len 9, cap 16  (小于256,*2)
00001000, len 8, cap 8

map

结构

map 底层是一个指针 *hmap

hash 值的位数取决于操作系统的位数(32 / 64)

image-20250508102747100

上图就是 bucket 的内存模型,HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。

hi 是进一步的 hash(对应 hash 值的高八位),通过对齐后的内存偏移计算 value 的位置

image-20250607223141061

溢出桶目的是为了减少扩容的次数(可能会预分配连续的内存作为溢出桶)

例如,有这样一个类型的 map:

map[int64]int8 // key: 8byte value: 1byte

考虑到内存对齐问题,按 8 个字节对齐:

如果按照 key/value/key/value/... 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 key/key/.../value/value/...,则只需要在最后添加 padding。

扩容

采用渐进式扩容的方式,避免一次性扩容带来的性能抖动:每次写入删除时迁移桶

nevacuate 就用来存储下一次扩容需要迁移的桶号

条件 扩容方式 目的
负载因子 > 6.5 双倍扩容(2×桶) 减少冲突,提高查询效率
溢出桶过多(≥ 桶数量) 等量扩容(整理) 减少溢出链,优化内存布局
  • 每个桶在不考虑溢出桶的情况下,可以存储最多 8 个 kv,为什么到 6.5 就需要扩容?平均 6.5 时,有些哈希碰撞非常集中的桶其实已经有很多溢出桶了
  • 为什么等量扩容是有效的?因为发生等量扩容是一般是 map 中由于删除操作过多产生了空洞,溢出桶很多并且不满

JDK8 及以后,Map 扩容也使用渐进式扩容,但是查询顺序是先新桶后旧桶;go 是先旧桶后新桶:因为 java 的迁移更加积极,不仅在写入和删除时执行迁移,还会在查询时也执行迁移

接口

值接收者和指针类型接收者

实现了值类型作为接收者的方法,会自动实现指针类型接收者的相同方法;指针类型接收者方法不能自动实现值类型的接受方法。

接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。

检查某个结构体的 值 / 指针 是否实现了某个接口:

var _ = (*MyInterface)(nil) // 检查指针类型的绑定

var _ = (MyInterface)(nil) // 值类型的绑定

iface 和 eface

iface 描述的接口包含方法

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized, functions
}

eface 描述空接口

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

_type :

type _type struct {
    // 类型大小
  size       uintptr
    ptrdata    uintptr
    // 类型的 hash 值
    hash       uint32
    // 类型的 flag,和反射相关
    tflag      tflag
    // 内存对齐相关
    align      uint8
    fieldalign uint8
    // 类型的编号,有bool, slice, struct 等等等等
  kind       uint8
  alg        *typeAlg
  // gc 相关
  gcdata    *byte
  str       nameOff
  ptrToThis typeOff
}

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:

type arraytype struct {
  typ   _type
  elem  *_type
  slice *_type
  len   uintptr
}

type chantype struct {
  typ  _type
  elem *_type
  dir  uintptr
}

type slicetype struct {
  typ  _type
  elem *_type
}

type structtype struct {
  typ     _type
  pkgPath name
  fields  []structfield
}

接口的动态类型和动态值

iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型动态值。而接口值包括动态类型动态值

只有当接口的动态类型和动态值都为 nil 时,这个接口值才会被认为是 nil

type Coder interface {
    code()
}

var c Coder // 这里的 c != nil

多态

同一种操作(函数)应用于不同的对象时,产生不同的影响

package main

import (
    "fmt"
)

type person interface {
    grow() int
    speak([]byte)
}

type student struct {
    name string
    age int
}
func (stu *student)grow() int {
    stu.age += 1
    return stu.age
}
func (stu *student)speak(words []byte) {
    fmt.Printf("student: %s", string(words))
}

type teacher struct {
    subject string
    age int
}
func (t *teacher)grow() int {
    t.age += 1
    return t.age
}
func (t *teacher)speak(words []byte) {
    fmt.Printf("teacher %s, %s", string(words), t.subject)
}

/* 可传入任意实现了 person 的结构体 */
func getGrown(p person) {
    fmt.Println(p.grow())
}
func say(p person) {
    p.speak([]byte("sssss"))
}

func main() {
    stu := &student{name: "bbb", age: 18}
    t := &teacher{subject: "math", age: 45}
    getGrown(stu)
    getGrown(t)

    say(stu)
    say(t)
}