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个,会将3int{1,2,3}转换成更原始的语句:

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

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

越界检查

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

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

相关推荐
用户34232323763172 小时前
开源!Go+Wails+Vue3 手搓一个 PLC 实时监控桌面工具
go
止语Lab3 小时前
为什么你的 Go TCP server P99 延迟这么高
go
Andy Dennis9 小时前
nsq学习记录
消息队列·go·nsq
韦胖漫谈IT11 小时前
选语言不是站队,是选适合问题的工具
java·python·ai·rust·go·技术落地
喵个咪1 天前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
夜悊1 天前
Go网络编程的学习代码示例:客户端/服务端(C/S)模型
go
审判长烧鸡1 天前
【AI问答】GO代码循环返值
go
捧 花1 天前
Eino框架记忆功能实现指南
go·agent·eino
Java陈序员1 天前
主流数据库通吃!一款开源实用的数据库备份管理工具!
react.js·postgresql·go
云浪1 天前
搞懂 Go WaitGroup:一篇文章彻底理解并发等待机制
后端·go