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

相关推荐
蜗牛丨11 小时前
Go Vue3 CMS管理后台(前后端分离模式)
mysql·docker·go·vue3·axios·gin·jwt·分页·跨域·ant design vue·log·gorm·otp动态码登录·validator·模型绑定·权限判断
yuguo.im2 天前
Dkron 架构与设计
架构·golang·go·dkron
桃园码工3 天前
2-测试bigcache做进程内缓存 --开源项目obtain_data测试
vscode·mysql·go·postman
ZQDesigned3 天前
在 Windows 和 macOS 上配置 Golang 语言环境
后端·go
楽码3 天前
只需一文:了解validator标签以轻松验证
后端·安全·go
煎鱼eddycjy5 天前
新提案:由迭代器启发的 Go 错误函数处理
go
煎鱼eddycjy5 天前
Go 语言十五周年!权力交接、回顾与展望
go
不爱说话郭德纲5 天前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星6 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架7 天前
golang高频面试真题
面试·go