Go 语言中的结构体、切片与映射:构建高效数据模型的基石

Go 复合数据类型

往期博客

Go语言新手村:轻松理解变量、常量和枚举用法

1. 数组

Go 语言中的数组是一个固定长度的数据结构,存储统一类型的元素序列。长度在创建时指定,且无法更改。数组中的元素可以通过索引访问。

1.1 基础使用方式

[长度]类型关键字

数组初始化必须设置长度!!!

go 复制代码
// var关键字声明
var intArr [5]int
fmt.Println(intArr)

// 短变量方式声明	
intList := [5]int{1, 2, 3, 4, 5}
fmt.Println(intList) // [1 2 3 4 5]
// 索引访问
fmt.Println(intList[2]) // 3

1.2 传递方式

数据是值传递,函数内部修改数组不影响原数组

go 复制代码
func array() {
	intList := [5]int{1, 2, 3, 4, 5}

	updateArray(intList)
	fmt.Println("修改方法外部:", intList)
  // 修改方法外部: [1 2 3 4 5]
}


func updateArray(arr [5]int) {
	arr[0] = 100
	fmt.Println("修改方法内部:", arr)
  // 修改方法内部: [100 2 3 4 5]
}

值传递会引发值拷贝的问题,如果数据量特别大,在拷贝的时候可能会有较大的性能损耗,在go语言中,解决这个问题的办法就是切片。

2. 切片

切片是一种动态数组,可以自动扩缩容。切片的底层其实是底层数组的引用。切片是一个结构体,包含三个元素:指向底层数组的指针、切片的长度、切片的容量。

2.1 初始化

[]类型关键字

刚刚介绍数组的时候,提到数组初始化必须指定长度,这也是因为切片初始化和数组初始化的代码类似,但是切片不需要指定长度。Go语言会认为没有指定长度的就是一个切片。

go 复制代码
// var 关键字初始化
var slice []int
// 追加元素
slice = append(slice, 1, 2, 3)
slice = append(slice, 4)
slice = append(slice, 5)
fmt.Println(slice) // [1 2 3 4 5]
// 追加并创建新的切片
newSlice := append(slice, 6)
fmt.Println(newSlice) // [1 2 3 4 5 6]

// make 关键字初始化
/// 1.指定类型、长度。容量默认和长度一致
makeSlice := make([]int, 5)
makeSlice[0] = 1
makeSlice[3] = 2
fmt.Println(makeSlice, "长度:", len(makeSlice), "容量:", cap(makeSlice))
// [1 0 0 2 0] 长度: 5 容量: 5

/// 2. 指定容量
	makeSliceCap := make([]int, 5, 10)
	makeSliceCap[1] = 10
	makeSliceCap[4] = 10
	fmt.Println(makeSliceCap, "长度:", len(makeSliceCap), "容量:", cap(makeSliceCap))
// [0 10 0 0 10] 长度: 5 容量: 10

// 短变量声明
shortSlice := []int{5, 4, 3, 2, 1}
	fmt.Println(shortSlice, "长度:", len(shortSlice), "容量:", cap(shortSlice))
// [5 4 3 2 1] 长度: 5 容量: 5

2.2 切片化

接触过python的开发者应该知道python中有一个数组切片操作,go语言中也支持,使用[start:end]形式对数组进行切片,使用方式如下

go 复制代码
// 初始切片
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("初始切片:", s, "len=", len(s), "cap=", cap(s))

// 1. s[n]:获取索引项
fmt.Println("s[3] =", s[3])
// 输出: 3

// 2. s[:]:全切片拷贝
sFull := s[:]
fmt.Println("s[:] =", sFull)
// 输出: [0 1 2 3 4 5 6 7 8 9]

// 3. s[low:]:从low到结尾
sLow := s[3:]
fmt.Println("s[3:] =", sLow, "len=", len(sLow), "cap=", cap(sLow))
// [3 4 5 6 7 8 9] len= 7 cap= 7

// 4. s[:high]:从开头到high
sHigh := s[:6]
fmt.Println("s[:6] =", sHigh, "len=", len(sHigh), "cap=", cap(sHigh))
// [0 1 2 3 4 5] len= 6 cap= 10

// 5. s[low:high]:指定范围
sRange := s[2:6]
fmt.Println("s[2:6] =", sRange, "len=", len(sRange), "cap=", cap(sRange))
// [2 3 4 5], len=4, cap=8

// 6. s[low:high:max]:限制容量
sCapLimit := s[2:6:8] // len=6-2=4, cap=8-2=6
fmt.Println("s[2:6:8] =", sCapLimit, "len=", len(sCapLimit), "cap=", cap(sCapLimit))
操作 含义
s[n] 切片s中索引位置为n的项
s[:] 从切片s的索引位置0到len(s)-1处所获得的切片
s[low:] 从切片s的索引位置low到len(s)-1处所获得的切片
s[:high] 从切片s的索引位置0到high处所获得的切片,len=high
s[low:high] 从切片s的索引位置low到high处所获得的切片,len=high-low
s[low:high:max] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low

切片实际上是对已经存在的数组进行切片操作,从同一个数组/切片创建的新切片指向的底层数组是一样的,修改一个会修改其他所有

go 复制代码
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr)
// [1 2 3 4 5 6 7 8 9 10]
slice := arr[2:5]
fmt.Println(slice, len(slice), cap(slice))
// [3 4 5] 3 8
slice[0] = 100
fmt.Println(arr, slice)
// [1 2 100 4 5 6 7 8 9 10] [100 4 5]
arr[3] = 200
fmt.Println(arr, slice)
// [1 2 100 200 5 6 7 8 9 10] [100 200 5]

如图定义了一个 arr 数组,然后对他创建一个 slice 切片,其中切片索引 0 指向arr[2],切片长度为 3,切片的容量就是从索引2到数组末尾的可用空间,也就是容量为 8。

修改切片中的元素,指向的原数组也会相应改变。反之修改原数组,指向他的切片也会改变

2.3 扩容

go 复制代码
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr)
// [1 2 3 4 5 6 7 8 9 10]
slice := arr[2:5]
fmt.Println(slice, len(slice), cap(slice))
// [3 4 5] 3 8

// 第一次append
slice = append(slice, 100, 200, 300)
fmt.Println("======= 扩容前 ========")
fmt.Println(slice, len(slice), cap(slice))
// [3 4 5 100 200 300] 6 8
fmt.Println(arr, slice)
// [1 2 3 4 5 100 200 300 9 10] [3 4 5 100 200 300]

// 第二次append
slice = append(slice, 400, 500, 600, 700, 800)
fmt.Println("======= 扩容后 ========")
fmt.Println(slice, len(slice), cap(slice))
// [3 4 5 100 200 300 400 500 600 700 800] 11 16
fmt.Println(arr, slice)
// [1 2 3 4 5 100 200 300 9 10] [3 4 5 100 200 300 400 500 600 700 800]

切片初始化和上一小节一样,长度为3,容量为8

第一次通过append函数进行元素追加,追加三个元素,长度为6,未超过容量8,因此直接修改底层数组。arr[5]arr[6]arr[7] 被覆盖为 100,200,300,证明切片与数组共享内存

第二次追加了5个元素,总长度需求 = 11,超过当前容量(cap=8),触发扩容机制

  1. 分配新数组(通常按 2×旧容量 规则,此处 8→16
  2. 复制旧数据到新数组
  3. 追加新元素
  4. 切片指针指向新数组

原数组 arr 不再变化,证明切片已脱离原数组

切片扩容源码

源码位于runtime包下的slice.go文件

go 复制代码
// oldPtr -> 指向原切片底层数组的指针
// newLen -> 新切片的长度
// oldCap -> 原切片的容量
// num    -> 追加的元素数量
// et     -> 切片元素类型的元数据
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
  // 计算原切片的长度
  oldLen := newLen - num
  // 竞态检测
  if raceenabled {
   callerpc := sys.GetCallerPC()
   racereadrangepc(oldPtr, uintptr(oldLen*int(et.Size_)), callerpc, abi.FuncPCABIInternal(growslice))
  }
  // 内存消毒检测
  if msanenabled {
   msanread(oldPtr, uintptr(oldLen*int(et.Size_)))
  }
  // 地址消毒检测
  if asanenabled {
   asanread(oldPtr, uintptr(oldLen*int(et.Size_)))
  }
  // 边界检测
  if newLen < 0 {
   panic(errorString("growslice: len out of range"))
  }

  // 零大小元素特殊处理
  if et.Size_ == 0 {
   return slice{unsafe.Pointer(&zerobase), newLen, newLen}
  }
  // 容量计算策略 (核心,见下方源码)
  newcap := nextslicecap(newLen, oldCap)

  // 内存对齐优化
  var overflow bool
  var lenmem, newlenmem, capmem uintptr
  noscan := !et.Pointers()
  switch {
  case et.Size_ == 1:
   lenmem = uintptr(oldLen)
   newlenmem = uintptr(newLen)
   capmem = roundupsize(uintptr(newcap), noscan)
   overflow = uintptr(newcap) > maxAlloc
   newcap = int(capmem)
  case et.Size_ == goarch.PtrSize:
   lenmem = uintptr(oldLen) * goarch.PtrSize
   newlenmem = uintptr(newLen) * goarch.PtrSize
   capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
   overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
   newcap = int(capmem / goarch.PtrSize)
  case isPowerOfTwo(et.Size_):
   var shift uintptr
   if goarch.PtrSize == 8 {
    shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
   } else {
    shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
   }
   lenmem = uintptr(oldLen) << shift
   newlenmem = uintptr(newLen) << shift
   capmem = roundupsize(uintptr(newcap)<<shift, noscan)
   overflow = uintptr(newcap) > (maxAlloc >> shift)
   newcap = int(capmem >> shift)
   capmem = uintptr(newcap) << shift
  default:
   lenmem = uintptr(oldLen) * et.Size_
   newlenmem = uintptr(newLen) * et.Size_
   capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
   capmem = roundupsize(capmem, noscan)
   newcap = int(capmem / et.Size_)
   capmem = uintptr(newcap) * et.Size_
  }
  if overflow || capmem > maxAlloc {
   panic(errorString("growslice: len out of range"))
  }

  // 内存分配策略
  var p unsafe.Pointer
  if !et.Pointers() {
   p = mallocgc(capmem, nil, false)
   memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
  } else {
   p = mallocgc(capmem, et, true)
   if lenmem > 0 && writeBarrier.enabled {
    bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
   }
  }
  
  // 数据迁移
  memmove(p, oldPtr, lenmem)

  return slice{p, newLen, newcap}
}


// newLen -> 新切片的长度
// oldCap -> 旧切片的容量
func nextslicecap(newLen, oldCap int) int {
    // 超大需求扩容:新的长度大于两倍旧的容量,直接采用所需的容量
    if newLen > 2*oldCap {
        return newLen
    }
    
 
    const threshold = 256
    // 小切片扩容:容量小于256的小切片,直接给双倍容量
    if oldCap < threshold {
        return 2 * oldCap
    }
    
    // 大切片扩容:渐进式的扩容,根据旧的容量基数进行扩容
    newcap := oldCap
    for newcap < newLen {
        newcap += (newcap + 3*threshold) / 4
    }
    return newcap
}

通过观察源码,频发触发扩容会消耗很多性能,因此建议在初始化的时候通过make,显式指定一个长度/容量

3. map

map是一种关联数据类型,也被称为哈希表或字典。map的所用是将一个键和值关联起来,以便快速的通过键找到对应的值

3.1 基础使用

map[键类型]值类型

使用var关键定义map,在赋值前必须用make进行初始化,否则会出现异常:panic: assignment to entry in nil map

go 复制代码
// var 关键字定义
var names map[int]string
// make初始化
names = make(map[int]string)
// 必须在make之后赋值
names[1] = "小明"
fmt.Println(names)
// map[1:小明]

students := map[int]string{
  1: "张三",
  2: "李四",
  3: "王五",
  4: "赵六",
}
students[5] = "小七"
fmt.Println(students)
// map[1:张三 2:李四 3:王五 4:赵六 5:小七]

ages := make(map[int]int)
ages[1] = 18
ages[2] = 19
ages[3] = 20
fmt.Println(ages)
// map[1:18 2:19 3:20]

// 删除
fmt.Println(students[1])
// 张三
delete(students, 1)
fmt.Println(students[1])
// 

// 遍历
for k, v := range students {
  fmt.Println(k, v)
}
// 2 李四
// 3 王五
// 4 赵六
// 5 小七

其中map是无序的,所以每次遍历的结果顺序都可能不一样

3.2 修改

map和数组不一样,它是引用类型,所以在方法中修改也会影响到原map

go 复制代码
func main() {
  students := map[int]string{
    1: "张三",
    2: "李四",
    3: "王五",
    4: "赵六",
  }
  fmt.Println(students)
  // map[1:张三 2:李四 3:王五 4:赵六]


  updateMap(students, 2, "小明")
  fmt.Println("方法外 -> ", students)
  // 方法外 ->  map[1:张三 2:小明 3:王五 4:赵六]
}

func updateMap(mapVal map[int]string, key int, value string) {
  mapVal[key] = value
  fmt.Println("方法中 -> ", mapVal)
  // 方法中 ->  map[1:张三 2:小明 3:王五 4:赵六]
}

4. 结构体

结构体是一种复合类型,用于将多个不同类型的数据组合在一起,可以聚合各种类型的变量。

4.1 定义

type 自定义结构体名 struct{}

go 复制代码
type student struct {
  id    int
  name  string
  age   int
  score float32
}

4.2 初始化

go 复制代码
// 零值初始化
var a student
fmt.Println(a) // {0  0 0}

// 短变量声明初始化
b := student{
  id:    1,
  name:  "小王",
  score: 90.0,
}
fmt.Println(b) // {1 小王 0 90}

// 初始化后直接赋值
b.name = "小明"
b.age = 18
fmt.Println(b) // {1 小明 18 90}

4.3 访问

go 复制代码
// '.'直接访问	
fmt.Println(b.name) // 小明 
fmt.Println(b.age) // 18

// 指针访问
p := &b
fmt.Println(p.name) // 小明 
fmt.Println(p.score) // 90

// 修改指针指向的对象,原对象也会变化
p.name = "小张"
fmt.Println(b.name, p.name) // 小张 小张
相关推荐
屁股割了还要学2 分钟前
【数据结构入门】堆
c语言·开发语言·数据结构·c++·考研·算法·链表
HuiSoul2001 小时前
Spring MVC
java·后端·spring mvc
lsx2024062 小时前
Vue.js 响应接口:深度解析与实践指南
开发语言
froginwe112 小时前
Vue.js 样式绑定
开发语言
摇滚侠3 小时前
面试实战 问题二十四 Spring 框架中循环依赖问题的解决方法
java·后端·spring
GetcharZp4 小时前
C++日志库新纪元:为什么说spdlog是现代C++开发者必备神器?
c++·后端
Algebraaaaa4 小时前
为什么C++主函数 main 要写成 int 返回值 | main(int argc, char* argv[]) 这种写法是什么意思?
开发语言·c++
三木水5 小时前
Spring-rabbit使用实战七
java·分布式·后端·spring·消息队列·java-rabbitmq·java-activemq
快乐就是哈哈哈5 小时前
一篇文章带你玩转 EasyExcel(Java Excel 报表必学)
后端