当前位置: 首页 > news >正文

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

Go 复合数据类型

往期博客

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

1. 数组

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

1.1 基础使用方式

[长度]类型关键字

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

// 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 传递方式

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

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] = 100fmt.Println("修改方法内部:", arr)// 修改方法内部: [100 2 3 4 5]
}

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

2. 切片

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

2.1 初始化

[]类型关键字

刚刚介绍数组的时候,提到数组初始化必须指定长度,这也是因为切片初始化和数组初始化的代码类似,但是切片不需要指定长度。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] = 10makeSliceCap[4] = 10fmt.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]形式对数组进行切片,使用方式如下

// 初始切片
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

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

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 扩容

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文件

// 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 boolvar lenmem, newlenmem, capmem uintptrnoscan := !et.Pointers()switch {case et.Size_ == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap), noscan)overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.Size_ == goarch.PtrSize:lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.Size_):var shift uintptrif goarch.PtrSize == 8 {shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63} else {shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31}lenmem = uintptr(oldLen) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap)<<shift, noscan)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault: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.Pointerif !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 := oldCapfor 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

// 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

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] = valuefmt.Println("方法中 -> ", mapVal)// 方法中 ->  map[1:张三 2:小明 3:王五 4:赵六]
}

4. 结构体

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

4.1 定义

type 自定义结构体名 struct{}

type student struct {id    intname  stringage   intscore float32
}

4.2 初始化

// 零值初始化
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 访问

// '.'直接访问	
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) // 小张 小张
http://www.dtcms.com/a/327413.html

相关文章:

  • apache+虚拟主机
  • windows git安装步骤
  • 深入剖析 React 合成事件:透过 onClick 看本质
  • Flutter UI Kits by Olayemi Garuba:免费开源的高质量UI组件库
  • C++中template、 implicit 、explicit关键字详解
  • Kimi K2 架构深度解析:万亿MoE模型的效率革命与智能体突破
  • Linux随记(二十二)
  • Notta:高效智能的音频转文字工具
  • 视频抽取关键帧算法
  • MR一体机(VST)预算思路
  • Linux的pthread怎么实现的?(包括到汇编层的实现)
  • AWT 事件监听中的适配器模式:从原理到实战的完整指南
  • Photoshop软件打开WebP文件格的操作教程
  • leecode2439 最小化数组中的最大值
  • 大数据中的数据压缩原理
  • 【解决apisix问题】
  • 快速了解词向量模型
  • RIOT、RT-Thread 和 FreeRTOS 是三种主流的实时操作系统
  • SpringMVC的原理及执行流程?
  • Bugku-CTF-web-留言板1
  • Linux网络--2.2、TCP接口
  • PMBT2907A,215 Nxp安世半导体 双极性晶体管 开关电源管理芯片
  • 蚁剑--安装、使用
  • C# 基于halcon的视觉工作流-章29-边缘提取-亚像素
  • 力扣.870优势洗牌解决方法: 下标排序​编辑力扣.942增减字符串匹配最长回文子序列牛客.背包问题(最大体积)力扣.45跳跃游戏II 另一种思考
  • 数据结构——线性表(核心操作,附代码)
  • vue项目封装axios请求,支持判断当前环境及判断token是否过期等等(详细教程,可复制粘贴代码)
  • cuda排序算法--双调排序(Bitonic_Sort)
  • 【数据库】 MySQL 表的操作详解
  • 蓝桥杯手算题和杂题简易做法