go源码-数据结构-数组

概念

数组是相同类型元素的结合,描述数组需要两个信息:元素类型、元素个数。

存储方式

无论数组放在栈上还是堆上,都是连续的一段内存。元素的变量指向内存的开始位置。(无论哪一种类型的数据,底层都是存储在内存中,以二进制的形式。只不过对于不同类型的数据,从内存读取之后采用对应的解析方式进行解析)

访问方式

底层访问数组中的元素时,根据三个信息(内存的开始位置、元素类型所占字节大小、访问第几个元素),来计算目标元素的地址。访问数组时,分三个步骤:加载数组、计算目标地址、进行操作(取值、赋值)

1-新建数组

新建数组源码地址:src/cmd/compile/internal/types/type.go:NewArray

go 复制代码
// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
	if bound < 0 {
		base.Fatalf("NewArray: invalid bound %v", bound)
	}
	t := newType(TARRAY)
	t.extra = &Array{Elem: elem, Bound: bound}
	if elem.HasShape() {
		t.SetHasShape(true)
	}
	return t
}

NewArray函数两个参数:元素类型、元素个数。常量TARRAY代表的是数组类型(名称源于TYPE+ARRAY),go语言将所有的类型封装成了一个结构体Type,newType方法被调用是传入常量,根据常量判断新建的元素类型。newType代码如下:

ini 复制代码
func newType(et Kind) *Type {  
    t := &Type{  
        kind: et,  
        width: BADWIDTH,  
    }  
    t.underlying = t  
    // TODO(josharian): lazily initialize some of these?  
    switch t.kind {  
    case TMAP:  
        t.extra = new(Map)  
    case TFORW:  
        t.extra = new(Forward)  
    case TFUNC:  
        t.extra = new(Func)  
    case TSTRUCT:  
        t.extra = new(Struct)  
    case TINTER:  
        t.extra = new(Interface)  
    case TPTR:  
        t.extra = Ptr{}  
    case TCHANARGS:  
        t.extra = ChanArgs{}  
    case TFUNCARGS:  
        t.extra = FuncArgs{}  
    case TCHAN:  
        t.extra = new(Chan)  
    case TTUPLE:  
        t.extra = new(Tuple)  
    case TRESULTS:  
        t.extra = new(Results)  
    }  
    return t  
}

有人会有疑问,为什么newType方法里的case里没有TARRAY等其他类型呢?

本人认为(对于map、func、chan等数据类型类说,他们的结构体里的字段就是一些指针,而指针的大小是可以确定的,也就是说,对于这些数据结构来说,他们新建的时候,大小是固定的,不确定的是他们的各个字段的指针指向的内容,这些内容是后面才需要初始化的,所以可以直接使用new()。而对于TARRAY、TINT8、TSTRING等,初始化的时候就需要确定内部细节以及根据输入确定内存分配的大小,所以不直接new(),而是手动创建。如newArray方法中的代码中的t.extra = &Array{Elem: elem, Bound: bound}).

如果数组元素小于等于四个,默认放在栈上,如果大于4个则将其移到静态区中。

scss 复制代码
func anylit(n ir.Node, var_ ir.Node, init *ir.Nodes) {
    case ir.OSTRUCTLIT, ir.OARRAYLIT:  
    n := n.(*ir.CompLitExpr)  
    if !t.IsStruct() && !t.IsArray() {  
        base.Fatalf("anylit: not struct/array")  
    }  
   
    if isSimpleName(var_) && len(n.List) > 4 {// 移动到静态区中
         
        // lay out static data  
        vstat := readonlystaticname(t)  

        ctxt := inInitFunction  
        if n.Op() == ir.OARRAYLIT {  
        ctxt = inNonInitFunction  
        }  
        fixedlit(ctxt, initKindStatic, n, vstat, init)  

        // copy static to var  
        appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, vstat))  

        // add expressions to automatic  
        fixedlit(inInitFunction, initKindDynamic, n, var_, init)  
        break  
    }
}

除此之外,在fixedlit函数中,还会对数组的生命方式进行优化,如果元素少于等于4个,会将[3]int{1,2,3}转换成更原始的语句:

css 复制代码
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

如果元素大于4个,会直接在静态存储区初始化数组,然后将地址赋值给数组变量,后续再复制到栈上。

越界检查

数据的越界检查包括编译时和运行时:对于数组的访问,如果使用整数或常量访问,能够直接在编译时检查出来。而如果使用变量访问,则只能在运行期间进行检查。在访问数组时的第二个步骤-(计算目标地址)之后,就可以检查出是否越界。

参考文献:《Go语言设计与实现》 @Draven

相关推荐
研究司马懿19 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto6 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题7 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo