Golang 03_Golang数组与切片

一、数组(array)

1.1 数组(array)简介

数组(array) 是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存空间来保存其中的元素,可以利用数组中元素的索引快速访问特定元素,常见的数组大多都是一维的 线性数组,而多维数组在数值和图形计算领域却有比较常见的应用。

数组有如下特点:

  • 元素的数据类型要相同;
  • 占用内存连续(线性);
  • 声明固定长度,不能动态变化;
  • 声明时,默认值为零值(0、""、false等);
  • 数组是值类型,默认情况下作为参数传递给函数时会拷贝,不影响原数组;

在Go语言中声明和使用数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 声明数组:
var a [5]int         // 声明一个长度为5的int类型数组,全部元素默认为0
a = [5]int{1, 2, 3}  // 将前三个元素初始化为1、2、3,后两个为元素默认为0

var numArr01 [3]int = [3]int{1, 2, 3} //在声明数组的时候直接赋值
var numArr02 = [3]int{1, 2, 3}
var numArr03 = [...]int{1, 2, 3}  //...表示自动识别
var numArr04 = [...]int{1: 800, 0: 900, 2:999}

b := [5]int{1, 2, 3, 4, 5} // 直接初始化并赋值

c := [...]int{1,2,3,4,5} // 隐式声明并初始化并赋值

// 使用数组:
x := a[0]    // 访问第一个元素
a[2] = 10    // 修改第三个元素

// 循环遍历数组:
//for循环遍历
for i := 0; i < len(a); i++ {
    fmt.Println(a[i])
}

//for - range遍历
for index,value := range a{
  fmt.Println(index, value)
}

// 数组作为函数参数传递:
func sum(a [5]int) int {
    s := 0
    for i := 0; i < len(a); i++ {
        s += a[i]
    }
    return s
}

x := sum([5]int{1, 2, 3, 4, 5})

多维数组:

1
2
var a [3][3]int     // 声明一个3x3的二维数组
a[0][0] = 1

在Go语言中,数组还可以与切片、指针和结构体等其他类型进行组合使用,以实现更复杂的数据结构和算法。由于数组在Go中使用频率没有切片那么高,所以一些更灵活的操作将在切片内容部分讲解。

1.2 数组(array)底层编译原理

要了解数组的底层的知识点,必须先了解Go的编译流程,Go的编译流程大致分为以下几个步骤(详见《Golang编译原理及编译过程》章节):

  1. 词法分析:将源代码转换成一个个的标记(token),包括关键字、标识符、常量、运算符等。
  2. 语法分析:将标记组合成抽象语法树(AST),AST是一个树形结构,代表了源代码的语法结构。
  3. 类型检查:检查代码中的类型错误,例如类型不匹配、未定义的变量等。
  4. 生成中间代码:将AST转换为中间代码(IR),Go使用SSA(Static Single Assignment)形式的IR,其每个变量只被赋值一次。
  5. 代码优化:对中间代码进行各种优化,包括常量折叠、死代码删除、内联函数等。
  6. 生成汇编代码:将IR转换为目标机器的汇编代码。
  7. 链接:将生成的目标文件与系统库、其他库链接生成可执行文件。

整个编译流程中,词法分析、语法分析、类型检查、中间代码生成和代码优化都是由编译器完成的,而生成汇编代码和链接则是由链接器完成的。

关于数组底层内容,将选取词法分析、语法分析、类型检查、生成中间代码等阶段的几个重要关键点讲解。

编译时数组类型解析: Go 语言的数组有两种不同的创建方式,一种是显式的指定数组大小,另一种是使用 […]T 声明数组:

1
2
array1 := [5]int{1,2,3,4,5}
array2 := [...]int{1,2,3,4,5}

在词法及语法解析时,上述几种方式声明的数组会被解析为 ArrayType``, 当遇到 […] 的声明时,其长度会被标记为 `nil`,将在后续阶段进行自动推断。 `ArrayType 的结构体定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// go 1.20.3   path:  src/cmd/compile/internal/syntax/nodes.go
//ArrayType 数组类型节点结构体
type (
  ......
  // [Len]Elem
  ArrayType struct {
    Len  Expr
    Elem Expr
    expr
  }
  ......
)

该结构体有两个字段 Len 、 Elem 以及 一个内嵌结构体expr:

  • Len 表示数组的长度,类型为 Expr,可以是一个整数常量或者是一个变量或表达式,当数组是一个省略号形式的变长数组时,Len 为 nil;
  • Elem 表示数组的元素类型,类型为 Expr,通常是一个基础类型或一个结构体类型等,Elem 字段也可以是一个数组类型,表示多维数组,对于一个空接口类型 interface{},它的 Elem 字段为 nil;
  • 内嵌结构体exp ,表示表达式的基础类型,其中包含了表达式的位置信息等;

Expr 是一个接口类型,是AST(抽象语法树)节点的一种类型,表示程序中的一个表达式,是语言的基本构建块之一。它们出现在各种语句和声明中,如赋值语句、条件语句、循环语句、函数调用等等,由于表达式是可以求值的,所以它们在程序中经常被用来计算和处理数据。例如,1+2、x+3、f() 等都是合法的表达式,它们的类型都实现了 Expr 接口。

数组被解析为ArrayType节点类型,使用的是函数 typeOrNil() 。

该函数是Go语言的解析器(parser)中的一个方法,返回值是一个表达式(Expr),表达式可以是不同类型的节点,例如指针(Indirect)、通道(ChanType)、数组(ArrayType)、映射(MapType)、结构体(StructType)、接口(InterfaceType)等。如果不能解析出一个类型,该函数返回 nil。

ypeOrNil()函数关于数组部分的源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// go 1.20.3   path:  src/cmd/compile/internal/syntax/parser.go
func (p *parser) typeOrNil() Expr {
    ......
  // 获取当前词法标记的位置信息
    pos := p.pos()
    switch p.tok {
    ......
    case _Lbrack:
    // '[' oexpr ']' ntype 或 '[' _DotDotDot ']' ntype,解析数组或切片类型
        p.next() // 向前读取一个词法标记
    // 如果下一个词法标记是右中括号,则表示是切片类型
        if p.got(_Rbrack) {
      // 解析切片类型并返回切片类型节点
            return p.sliceType(pos)
        }
    // 否则解析数组类型并返回数组类型节点
        return p.arrayType(pos, nil)
    ......
    return nil
}
  
/**
    // go 1.20.3   path:  src/cmd/compile/internal/syntax/parser.go
    该函数用于解析数组类型,并返回一个Expr表示该类型
    pos表示数组类型的位置,len表示数组的长度
*/
func (p *parser) arrayType(pos Pos, len Expr) Expr {
    ......
  // 如果长度为空并且没有出现省略号(_DotDotDot),则解析表达式
    if len == nil && !p.got(_DotDotDot) {
        p.xnest++
        len = p.expr()
        p.xnest--
    }
    ......
    // 要求下一个标记为右方括号
    p.want(Rbrack)
    // 创建一个新的数组类型
    t := new(ArrayType)
    t.pos = pos
    t.Len = len
    t.Elem = p.type()
    return t
}

通过上述代码操作,数组的声明被解析为 ArrayType类型的表达式。

types2.Array 在 Go 语言中,数组类型可以有固定的长度也可以是不定长的(即长度为 nil),但是它们在类型系统中被表示为不同的类型。这是因为在数组类型的定义中,它们的长度是数组类型的一个组成部分,不同长度的数组类型是不兼容的。

当编译器遇到一个长度为 nil 的数组类型时,它会将其表示为 types2.Array 类型。

这是因为在类型检查的过程中,类型检查器需要为这种不定长数组类型分配一个类型,并且需要检查其元素类型是否匹配。这个类型在内部是用 types2.Array 来表示的,因为 types2.Array 类型可以存储一个不确定的长度,并且可以在之后被转换为一个具有确定长度的 types.Array 类型。

这种转换发生在类型检查阶段,因为只有在这个阶段,编译器才有足够的信息来确定数组类型的长度。

在类型检查阶段,编译器对语法树中的表达式节点进行类型检查,根据表达式的上下文推导表达式的类型,并进行类型转换(type conversion)等操作。

检查时,如果是ArrayType类型,且其长度Len为nil时,会初始化一个types2.Array并将其长度标记为-1,然后通过check.indexedElts(e.ElemList, utyp.elem, utyp.len)返回数组长度n并赋值给Len,完成自动推断。

types2.Array结构体:

1
2
3
4
5
// go 1.20.3   path:  src/cmd/compile/internal/types2/array.go
type Array struct {
    len  int64
    elem Type
}

函数exprInternal是Go类型检查器中的核心函数之一,用于对表达式进行类型检查和求值。

该函数对数组类型的节点表达式检查的相关源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (check *Checker) exprInternal(x *operand, e syntax.Expr, hint Type) exprKind {
    ......
    //通过类型断言判断表达式 e 的具体类型
    switch e := e.(type) {
    ......
    //如果类型是*syntax.CompositeLit类型,即语法树节点类型
    case *syntax.CompositeLit:
        //声明了两个 Type 类型的变量 typ 和 base,用于保存表达式 e 的类型
        var typ, base Type
        switch {
        //如果 e.Type 不为空,则将其转换为Type类型,并将其赋值给变量 typ 和 base
        case e.Type != nil:
      //e.Type为 *syntax.ArrayType 类型且数组长度 Len 为 nil,则将typ设置为&types2.Array,
            if atyp, _ := e.Type.(*syntax.ArrayType); atyp != nil && atyp.Len == nil {
                typ = &Array{len: -1, elem: check.varType(atyp.Elem)}
                base = typ
                break
            }
            typ = check.typ(e.Type)
            base = typ
        ......
        }
        //通过 coreType 函数获取 base 的基础类型信息
        switch utyp := coreType(base).(type) {
        ......
        //如果基础类型是 *Array 类型,即数组类型
        case *Array:
            //首先检查其元素类型是否为 nil,如果是则报错,表示这是无效的递归类型
            if utyp.elem == nil {
                check.error(e, InvalidTypeCycle, "invalid recursive type")
                goto Error
            }

            //通过 indexedElts 函数计算复合字面量表达式的元素数量
            n := check.indexedElts(e.ElemList, utyp.elem, utyp.len)
            //如果数组类型的长度为负数,则设置其长度为计算得到的元素数量
            if utyp.len < 0 {
                utyp.len = n
        //如果 e.Type 不为空,则记录类型和值信息
                if e.Type != nil {
                    check.recordTypeAndValue(e.Type, typexpr, utyp, nil)
                }
            }
        ......
        }
        ......
    }
    ......
}

types.Array 与 types2.Array转换 在Go编译器的类型检查过程中,会在 go/types 包中进行类型推断和类型检查。

其中,类型检查器需要使用 types.Array 类型进行数组类型的表示和处理,但在 types2 包中的 Array 类型是用表示不定长的数组的,在设计时可能存在一些历史原因或者设计考虑,导致其在 types 包中无法直接使用。

因此,在 types 包中提供了 types.NewArray() 函数,可以将 types2.Array 转换为 types.Array 类型,以便于后续类型检查器进行处理。通过这种方式,可以将 types2 包中定义的数据类型与 types 包中的类型体系进行衔接,保证类型检查器的正常运行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//go1.20.3 path:src/cmd/compile/internal/types/type.go
type Array struct {
    Elem  *Type // element type
    Bound int64 // number of elements; <0 if unknown yet
}

func NewArray(elem *Type, bound int64) *Type {
    //如果bound小于0,则抛出一个致命错误。
    if bound < 0 {
        base.Fatalf("NewArray: invalid bound %v", bound)
    }
    //创建一个新的Type结构体,并将其类型设置为TARRAY,表示这是一个数组类型
    t := newType(TARRAY)

    //将t的extra字段设置为一个指向Array结构体的指针,
    //其中Elem字段指向elem所指向的Type结构体,表示该数组的元素类型;
    //Bound字段设置为bound,表示该数组的长度。
    t.extra = &Array{Elem: elem, Bound: bound}
    //如果elem所指向的Type结构体具有类型参数(即泛型类型),则将t的HasTParam字段设置为true
    if elem.HasTParam() {
        t.SetHasTParam(true)
    }
    //如果elem所指向的Type结构体具有形状信息(即元组类型),则将t的HasShape字段设置为true
    if elem.HasShape() {
        t.SetHasShape(true)
    }
    return t
}

而在生成中间结果时,types2.Array最终会通过types.NewArray()转换成types.Array类型:

1
2
3
4
5
6
7
8
//go1.20.3 path: src/cmd/compile/internal/noder/types.go
func (g *irgen) typ0(typ types2.Type) *types.Type {
    switch typ := typ.(type) {
    ...
    case *types2.Array:
        return types.NewArray(g.typ1(typ.Elem()), typ.Len())
    ...
}

编译时数组字面量初始化 数组类型解析可以得到数组元素的类型Elem以及数组长度Bound, 而数组字面量的初始化是在编译时类型检查阶段完成的,通过函数tcComplit -> typecheckarraylit循环字面量分别进行赋值。

tcComplit函数的作用是对一个复合字面量(composite literal)进行类型检查,它返回一个经过检查的 ir.Node 节点。

该函数相关数组的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// go 1.20.3   path: /src/cmd/compile/internal/typecheck/expr.go
func tcCompLit(n *ir.CompLitExpr) (res ir.Node) {
    ...
    //获取 CompLitExpr 的类型 t
    t := n.Type()
  
    //通过调用断言函数AssertfAt确保类型t不为 nil。
    //如果为 nil,则输出错误信息 "missing type in composite literal"。
    base.AssertfAt(t != nil, n.Pos(), "missing type in composite literal")
  
    //根据类型 t 的种类,执行不同的处理
    switch t.Kind() {
    ...
    //如果 t的种类为 TARRAY,即数组
    case types.TARRAY:
        //对数组字面量的元素进行类型检查,且将数组字面量中的每个元素转换为指定类型的值,并确保所有元素都具有相同的类型
        typecheckarraylit(t.Elem(), t.NumElem(), n.List, "array literal")
        //将操作符设置为 OARRAYLIT
        n.SetOp(ir.OARRAYLIT)
    ...
    return n
}

typecheckarraylit函数的作用是将数组字面量中的每个元素转换为指定类型的值,并确保所有元素都具有相同的类型。

它接受四个参数:

  • elemType 表示数组元素的类型
  • bound 表示数组的上限长度
  • elts 是一个 ir.Node 类型的切片,表示数组的元素列表
  • ctx 是一个字符串,用于在编译错误信息中提供上下文信息

该函数源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// go 1.20.3  path: src/cmd/compile/internal/typecheck/typecheck.go
func typecheckarraylit(elemType *types.Type, bound int64, elts []ir.Node, ctx string) int64 {
    ...
    //遍历数组字面量 elts 中的每个元素
    for i, elt := range elts {
        //设置元素 elt 的位置信息
        ir.SetPos(elt)
        //将元素 elt 的值赋给变量r
        r := elts[i]
        ......
        //对变量r的值进行表达式转换
        r = Expr(r)
        //将变量r的值转换为指定类型 elemType 的值
        r = AssignConv(r, elemType, ctx)
        ...
}

编译时初始化字面量的优化 在类型检查(Type Checking)阶段,为了减少在程序运行期间的开销,在编译期间优化了复合字面量的初始化过程。

在编译期间,编译器将复合字面量转化为静态或动态的数据结构,并将其放入程序的数据段或堆中。而在运行期间,程序可以直接使用这些静态或动态的数据结构,而不需要再次计算或初始化。

编译器会在负责初始化字面量的 cmd/compile/internal/gc.anylit函数中做两种不同的优化:

  1. 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上; 假设代码需要初始化 [5]int{1, 2, 3, 4, 5},那么可以将上述过程理解成以下的伪代码:
1
2
3
4
5
6
7
var arr [5]int
statictmp_0[0] = 1
statictmp_0[1] = 2
statictmp_0[2] = 3
statictmp_0[3] = 4
statictmp_0[4] = 5
arr = statictmp_0
  1. 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出; 假设代码需要初始化 [3]int{1, 2, 3},那么可以将上述过程理解成以下的伪代码:
1
2
3
4
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

anylit关于数组初始化优化相关代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func anylit(n ir.Node, var_ ir.Node, init *ir.Nodes) {
    t := n.Type()
    switch n.Op() {
    ......
    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 {
            vstat := readonlystaticname(t)
            ctxt := inInitFunction
            if n.Op() == ir.OARRAYLIT {
                ctxt = inNonInitFunction
            }
            // 进行静态优化
            fixedlit(ctxt, initKindStatic, n, vstat, init)
            // 将静态变量赋值给目标变量
            appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, vstat))
            // 进行动态优化
            fixedlit(inInitFunction, initKindDynamic, n, var_, init)
            break
        }

        var components int64
        if n.Op() == ir.OARRAYLIT {
            components = t.NumElem()
        } else {
            components = int64(t.NumFields())
        }
        // 如果变量名是简单标识符或者字面量表达式中的元素数量小于目标结构体或数组中的元素数量,则给目标变量赋值为nil
        if isSimpleName(var_) || int64(len(n.List)) < components {
            appendWalkStmt(init, ir.NewAssignStmt(base.Pos, var_, nil))
        }

        // 执行局部代码优化
        fixedlit(inInitFunction, initKindLocalCode, n, var_, init)
    ......
    }
}

上述代码中,还有个非常重要的函数:fixedlit,该函数是 Go 语言内部编译器(compiler)的一部分,用于处理 Go 语言程序中的常量(constant)初始化过程。在 Go语言中,常量可以通过字面值(literal value)或表达式(expression)进行定义,但它们的值必须在编译时确定。因此,在程序编译时,需要对所有常量进行求值,以生成相应的常量值。这个过程是由编译器内部的常量解析器(constant resolver)完成的,而 fixedlit 函数则是其中的一部分。

fixedlit 函数重点源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
func fixedlit(ctxt initContext, kind initKind, n *ir.CompLitExpr, var_ ir.Node, init *ir.Nodes) {
    // 判断 var_ 是否为空标识符 _
    isBlank := var_ == ir.BlankNode
    var splitnode func(ir.Node) (a ir.Node, value ir.Node)
    switch n.Op() {
    //如果n的操作符是 OARRAYLIT 或者 OSLICELIT,则说明n表示一个数组或者一个切片
    case ir.OARRAYLIT, ir.OSLICELIT:
        var k int64
        // 定义一个匿名函数 splitnode,用于分离数组或切片中的每个元素
        splitnode = func(r ir.Node) (ir.Node, ir.Node) {
            if r.Op() == ir.OKEY {
                // 如果该元素是 key-value 形式,如 a:[2]int{0:1, 1:2},则需要解析key值
                kv := r.(*ir.KeyExpr)
                k = typecheck.IndexConst(kv.Key)
                if k < 0 {
                    // key 值必须是非负整数
                    base.Fatalf("fixedlit: invalid index %v", kv.Key)
                }
                r = kv.Value
            }
            // 构造IndexExpr节点,表示当前元素的索引, 如 a[0],a[1],a[2]
            a := ir.NewIndexExpr(base.Pos, var_, ir.NewInt(k))
            k++
            if isBlank {
                //如果当前元素没有对应变量,则返回 BlankNode 作为变量占位符
                return ir.BlankNode, r
            }
            // 返回变量和值
            return a, r
        }
    ......
    }

    for _, r := range n.List {
        // 分割节点,提取数组/切片/键值对的下标和值
        a, value := splitnode(r)
        // 如果是匿名变量并且右值没有副作用,则继续处理下一个节点
        if a == ir.BlankNode && !staticinit.AnySideEffects(value) {
            continue
        }
        //处理不同类型的右值, 如果右值是一个复合字面量,则递归处理
        switch value.Op() {
        ......
        //如果是数组/结构体字面量,递归调用 fixedlit 函数
        case ir.OARRAYLIT, ir.OSTRUCTLIT:
            value := value.(*ir.CompLitExpr)
            fixedlit(ctxt, kind, value, a, init)
            continue
        }

        // 判断右值是否为常量节点
        islit := ir.IsConstNode(value)

        // 如果当前为静态初始化且右值不是常量,则继续处理下一个节点
        // 如果当前为动态初始化且右值是常量,则继续处理下一个节点
        if (kind == initKindStatic && !islit) || (kind == initKindDynamic && islit) {
            continue
        }

        // 构造赋值语句节点
        ir.SetPos(a)
        as := ir.NewAssignStmt(base.Pos, a, value)
        as = typecheck.Stmt(as).(*ir.AssignStmt)

        // 根据不同的初始化类型,生成不同的赋值语句
        switch kind {
        // 静态初始化
        case initKindStatic:
            genAsStatic(as)  // 如果右值是常量,则直接将赋值语句添加到静态初始化列表中
        // 动态初始化
        case initKindDynamic, initKindLocalCode:
            // 如果右值是常量,则直接将赋值语句添加到动态初始化列表中
            appendWalkStmt(init, orderStmtInPlace(as, map[string][]*ir.Name{}))
        default:
            base.Fatalf("fixedlit: bad kind %d", kind)
        }
    }
}

该函数大致流程为:

  • 根据初始化上下文 ctxt 和初始化类型 kind,分别设置只读变量的标识和初始化标识。
  • 根据常量表达式的类型 n.Op(),分别处理数组字面值、结构体字面值和切片字面值,以及其它类型的字面值。
  • 如果常量表达式的类型为数组、结构体或切片字面值,则遍历它们的元素,并对每个元素递归调用 fixedlit 函数。对于结构体字面值,还需要遍历它们的字段并递归调用 fixedlit 函数。
  • 如果常量表达式的类型为其它类型的字面值,且其值可以通过常量表达式求值,则将求值结果存储到常量节点中。否则,将常量节点设置为 nil。

总体来说,函数 fixedlit 的作用是对常量表达式进行求值,并将求得的结果存储到常量节点中。它还会递归处理复合类型的字面值,并对每个元素和字段递归调用 fixedlit 函数。函数中使用的变量和参数都有明确的含义,比如 ctxt 表示初始化上下文,kind 表示初始化类型,n 表示常量表达式节点,var_ 表示常量节点等。

编译时数组索引越界检查 数组访问越界是非常严重的错误,Go在数组越界方面有如何方案:

  • 字面量访问下标:无论是数组的寻址还是赋值,都是在编译阶段完成的 Go 语言中可以在编译期间的静态类型检查判断数组越界,如果访问越界在编译时就无法通过检查, 此检查通过 typecheck1函数处理,关键源码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// go 1.20.3  path: src/cmd/compile/internal/typecheck/typecheck.go
func typecheck1(n ir.Node, top int) ir.Node {
  ......
  switch n.Op() {
  ......
  case ir.OINDEX:
        n := n.(*ir.IndexExpr)
        return tcIndex(n)
  ......
  }
}

// go 1.20.3  path: src/cmd/compile/internal/typecheck/expr.go
func tcIndex(n *ir.IndexExpr) ir.Node {
    //调用Expr 函数对被索引的对象进行语义分析
    n.X = Expr(n.X)
    //再调用 DefaultLit 函数对结果进行类型推导
    n.X = DefaultLit(n.X, nil)
    //调用 implicitstar 函数对结果进行处理
    n.X = implicitstar(n.X)
    l := n.X
    n.Index = Expr(n.Index)
    r := n.Index
    t := l.Type()
    //获取被索引的对象的类型,并判断该类型是否为 nil,或者索引值的类型是否为 nil,如果是,则返回 nil 并结束函数
    if t == nil || r.Type() == nil {
        n.SetType(nil)
        return n
    }
    // 使用 switch 语句块,检查类型 t 的种类
    switch t.Kind() {
    ......
    // 如果类型是字符串类型、数组类型或切片类型
    case types.TSTRING, types.TARRAY, types.TSLICE:
        // 调用 indexlit 函数对索引表达式中的索引进行处理
        n.Index = indexlit(n.Index)
        // 如果类型是字符串类型,将索引表达式的类型设置为 ByteType,否则设置为元素类型
        if t.IsString() {
            n.SetType(types.ByteType)
        } else {
            n.SetType(t.Elem())
        }
        // 创建一个字符串变量 why,设置为 "string"
        why := "string"
        // 如果类型是数组类型,将 why 设置为 "array",如果是切片类型,将 why 设置为 "slice"
        if t.IsArray() {
            why = "array"
        } else if t.IsSlice() {
            why = "slice"
        }
        // 如果索引表达式的类型不为空且不是整数类型,输出错误信息
        if n.Index.Type() != nil && !n.Index.Type().IsInteger() {
            base.Errorf("non-integer %s index %v", why, n.Index)
            return n
        }
        // 如果索引表达式未绑定且是整数类型的常量
        if !n.Bounded() && ir.IsConst(n.Index, constant.Int) {
            x := n.Index.Val()
            // 如果索引小于 0,输出错误信息
            if constant.Sign(x) < 0 {
                base.Errorf("invalid %s index %v (index must be non-negative)", why, n.Index)
                // 如果类型是数组类型且索引大于等于数组长度,输出错误信息
            } else if t.IsArray() && constant.Compare(x, token.GEQ, constant.MakeInt64(t.NumElem())) {
                base.Errorf("invalid array index %v (out of bounds for %d-element array)", n.Index, t.NumElem())
                // 如果类型是字符串类型且索引大于等于字符串长度,输出错误信息
            } else if ir.IsConst(n.X, constant.String) && constant.Compare(x, token.GEQ, constant.MakeInt64(int64(len(ir.StringVal(n.X))))) {
                base.Errorf("invalid string index %v (out of bounds for %d-byte string)", n.Index, len(ir.StringVal(n.X)))
                // 如果索引溢出,输出错误信息
            } else if ir.ConstOverflow(x, types.Types[types.TINT]) {
                base.Errorf("invalid %s index %v (index too large)", why, n.Index)
            }
        }
        ......
    }
    return n
}
  • 编译器无法提前发现错误,需要 Go 语言运行时阻止 数组和字符串的一些简单越界错误都会在编译期间发现,例如:直接使用整数或者常量访问数组;但是如果使用变量去访问数组或者字符串时,编译器就无法提前发现错误,我们需要 Go 语言运行时阻止不合法的访问。

Go 语言运行时在发现数组、切片和字符串的越界操作会由运行时的 runtime.panicIndex和 runtime.goPanicIndex触发程序的运行时错误并导致崩溃退出:

1
2
3
4
5
6
7
8
9
TEXT runtime·panicIndex(SB),NOSPLIT,$0-8
    MOVL    AX, x+0(FP)
    MOVL    CX, y+4(FP)
    JMP    runtime·goPanicIndex(SB)

func goPanicIndex(x int, y int) {
    panicCheck1(getcallerpc(), "index out of range")
    panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex})
}

当数组的访问操作 OINDEX 成功通过编译器的检查后,会被转换成几个 SSA 指令,假设有如下所示的 Go 语言代码,通过如下的方式进行编译会得到 ssa.html 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

func outOfArray() int {
    arr := [3]int{1, 2, 3}
    i := 2
    elem := arr[i]
    return elem
}

func main() {}

$ GOSSAFUNC=outOfRange go build main.go
dumped SSA to ./ssa.html

start 阶段生成的 SSA 代码就是优化之前的第一版中间代码,下面展示的部分是 elem := arr[i] 对应的中间代码,在这段中间代码中我们发现 Go 语言为数组的访问操作生成了判断数组上限的指令 IsInBounds 以及当条件不满足时触发程序崩溃的 PanicBounds :

IsInBounds 函数用于检查索引是否在数组或切片的有效范围内。如果索引越界,它将返回 false,否则返回 true PanicBounds 函数用于处理索引越界情况。如果索引越界,它将引发一个运行时 panic。如果索引没有越界,它什么也不做 SSA代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
start
b1:-
......
v20 (6) = LocalAddr <*[3]int> {arr} v2 v19
v21 (6) = IsInBounds <bool> v13 v10
If v21 → b2 b3 (likely) (6)
b2: ← b1-
v24 (6) = PtrIndex <*int> v20 v13
v25 (6) = Copy <mem> v19
v26 (6) = Load <int> v24 v25 (elem[int])
v27 (7) = MakeResult <int,mem> v26 v25
Ret v27 (+7)
b3: ← b1-
v22 (6) = Copy <mem> v19
v23 (6) = PanicBounds <mem> [0] v13 v10 v22
Exit v23 (6)
name i[int]: v13
name elem[int]: v26

编译器会将 PanicBounds 指令转换成上面提到的 runtime.panicIndex函数,当数组下标没有越界时,编译器会先获取数组的内存地址和访问的下标、利用 PtrIndex 计算出目标元素的地址,最后使用 Load 操作将指针中的元素加载到内存中。

当然只有当编译器无法对数组下标是否越界无法做出判断时才会加入 PanicBounds 指令交给运行时进行判断,在使用字面量整数访问数组下标时会生成非常简单的中间代码,当我们将上述代码中的 arr[i] 改成 arr[2] 时,就会得到如下所示的代码:

1
2
3
4
5
6
7
8
9
start
b1:-
......
v20 (5) = LocalAddr <*[3]int> {arr} v2 v19
v21 (+5) = PtrIndex <*int> v20 v13
v22 (5) = Load <int> v21 v19 (elem[int])
v23 (+6) = MakeResult <int,mem> v22 v19
Ret v23 (+6)
name elem[int]: v22

Go 语言对于数组的访问还是有着比较多的检查的,它不仅会在编译期间提前发现一些简单的越界错误并插入用于检测数组上限的函数调用,还会在运行期间通过插入的函数保证不会发生越界。

总结:

  • 数组是go语言中的特殊类型,其与其他语言不太一样。他不可以添加,但是可以获取值,获取长度。
  • 数组的拷贝都是值拷贝,因此不要尽量不要进行大数组的拷贝。
  • 常量的下标以及某一些变量的下标的访问越界问题可以在编译时检测到,但是变量的下标的数组越界问题只会在运行时报错。
  • 数组的声明中,存在一个语法糖。[…]int{2,3,4},但是本质本没有什么差别
  • 在编译期的优化阶段,还会进行重要的优化。当数组的长度小于4时,在运行时会在栈中进行初始化。当数组的长度大于4,会在静态区初始化数组
  • 其实我们在go语言中对于数组用得较少,而是更多的使用切片

二、切片(slice)

2.1 切片(slice)简介

在Go语言中,切片(Slice) 是一种数据结构,它是对数组一个连续片段的引用,这个数组称为切片的底层数组。

切片和数组的关系是非常紧密的。在Go语言中,数组是一个固定长度的具有相同类型元素序列,而切片则是一个可变长度的具有相同类型元素序列。

切片是建立在数组之上的,它提供了动态数组的功能,可以根据需要动态地增加或缩小切片的长度。

切片(Slice)是Go语言中非常重要的数据结构之一,它常用于对数组进行操作,提供了方便的动态扩容、切片、追加等操作。下面是一些切片的使用方法和场景,以及相应的Go语言代码示例。

slice 里面存放着3种东西:

  • 底层数组中索引位的内存地址
  • len 存放切片的长度
  • cap 存放切片的总容量,一般为长度的一倍以上

**切片(slice) 是一个引用类型的,和之前的值数据类型不同的是(值类型是把类型值存放在变量中),而切片相当于是一个软连接的存在,如果修改了slice的值,那么底层数组的值也会被修改。

1)切片声明与创建 切片类型的声明方式与数组有一些相似,不过由于切片的长度是动态的,所以声明时只需要指定切片中的元素类型,如下所示:

1
2
var s []int     // 声明一个整型切片
var s1 []string  // 声明一个字符串切片

一个切片在初始化之前为空切片(nil),长度为0。

切片定义完成后,还不能使用,因为本身是一个空的。

使用时通过make关键字来进行初始化切片为其分配一个新的底层数组,或者是基于指定的数组进行切片化:

可以以已有数组或切片创建并初始化新切片

1
2
3
4
5
6
var slice = arr[startindex:endlindex] //切片初始化时,从arr数组下标为startindex,取值到下标为endindex的元素,不包含arr[endlindex]

var intArr [5]int = [...]int{1,2,3,4,5} //这里定义一个数组

slice1 := intArr[1:4]      // 基于数组生成一个切片 其中[1:4]表示数组中1、2、3索引位的数据,不包括4索引
slice2 := slice1[1:3]      // 基于切片,我们生成一个切片 其中[1:3]表示切片中1、2索引位的数据,不包括3索引

可以在声明切片时直接初始化切片,如下表示声明一个int切片,初始化值为{1, 2, 3},类似make的功能:

1
2
3
s := []int {1, 2, 3} 
var s1 []int = []int {1, 2, 3}
var s2  = []int {1, 2, 3}

还可以使用 make([],len,[cap]) 函数来创建一个指定长度和容量的切片。

1
2
3
4
5
// 创建一个长度为3、容量为3的字符串切片
slice := make([]string, 3)

// 创建一个长度为3、容量为5的整形切片
slice1 := make([]int, 3, 5)

make的参数:

  • type:数据类型
  • len:切片元素数量
  • cap:指定切片容量,该参数可选,如果要分配了cap,则要求cap至少大于等于len

也可使用 applend 函数添加元素进行初始化

1
2
var slice []int
slice = append(slice, 10)

还可以直接通过数组或切片来创建一个新的切片,新切片的长度等于从原始数组或切片中指定的开始和结束索引之间的元素个数,容量等于原始数组或切片的长度减去开始索引(s := arr[startIndex:endIndex])。例如:

1
2
3
4
5
6
7
8
// 创建一个包含5个整数的数组
arr := [5]int{1, 2, 3, 4, 5}

// 创建一个从arr[1]开始到arr[3]结束的切片
slice1 := arr[1:4]

// 创建一个新切片,容量等于原始切片的长度
slice2 := slice1[:]

需要注意的是,当直接从另一个切片创建一个新的切片时,两个切片将共享相同的底层数组。因此,修改一个切片的元素也会影响到另一个切片。如下:

1
2
3
4
5
6
7
8
9
// 创建一个包含5个整数的数组
arr := [5]int{1, 2, 3, 4, 5}
slice1 := arr[1:4]         // 基于数组生成一个切片 其中[1:4]表示数组中1、2、3索引位的数据,不包括4索引
slice2 := slice1[1:3]      // 基于切片,我们生成一个切片 其中[1:3]表示切片中1、2索引位的数据,不包括3索引

slice1[0] = 6
fmt.Println(arr)    // 输出:[1 6 3 4 5]
fmt.Println(slice1) // 输出:[6 3 4]
fmt.Println(slice3) // 输出:[6 3]

Tips: 这种方式创建的切片,在切片容量改变(扩大)之前,其底层数组都是 intArr( slice的底层数组时 intArr 数组从第一个元素开始)。

  • 切片引用数组/切片的技巧
1
2
3
4
var slice = arr[startindex:endlindex]
var slice1 = arr[0:end]          //可以简写成 var slice = arr[:end]
var slice2 = arr[start:len(arr)] //可以简写成 var slice = arr[start:]
var slice3 = arr[0:len(arr)]     //可以简写: var slice = arr[:]

2)获取却得长度(len)和容量(cap)

  • len 函数用于获取切片的长度(和数组一样)
  • cap函数用于获取切片的容量(cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素)
1
2
len(slice)
cap(slice)

3)访问切片元素 可以使用切片的索引操作符[]来访问切片中的元素。切片的索引从0开始,最大值是切片长度减1。例如:

1
2
3
4
5
6
7
8
9
// 创建一个包含3个元素的整数数组
arr := [3]int{1, 2, 3}

// 创建一个包含arr[1]和arr[2]的切片
slice := arr[1:3]

// 访问切片中的元素
fmt.Println(slice[0]) // 输出:2
fmt.Println(slice[1]) // 输出:3

4)切片的遍历 可以使用 for循环 或者 for-range 来遍历切片中的元素。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 创建一个包含5个整数的切片
s := []int{1, 2, 3, 4, 5}

// for循环遍历切片
for i := 0; i < len(s); i++ {
  println(s[i])
}

// for range遍历切片
for key, value := range s {
  println(key, value)
}

5)切片追加元素 以使用内置的append函数向切片中追加元素,如果切片的容量不够,则会自动扩容。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 创建一个空的切片
var slice []int

// 向切片中追加元素
slice = append(slice, 1, 2, 3)

// 输出切片中的元素
fmt.Println(slice) // 输出:[1 2 3]

// 向切片中追加元素
slice = append(slice, 4)
fmt.Println(slice) // 输出:[1 2 3 4]

append操作的底层原理:

  1. 切片append操作的本质就是对数组进行扩容
  2. go底层会创建一下新的数组,newArr(安装扩容后大小)
  3. 将slice原来包含的元素拷贝到新的数组newArr
  4. slice重新引用到newArr
  5. 注意newArr是在底层维护的,不可见

6)切片的复制 可以使用内置的copy函数将一个切片中的元素复制到另一个切片中,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 创建一个包含3个元素的整数数组
arr1 := [3]int{1, 2, 3}

// 创建一个包含arr1[1]和arr1[2]的切片
slice1 := arr1[1:3]

// 创建一个长度为2、容量为4的空切片
slice2 := make([]int, 2, 4)

// 将slice1中的元素复制到slice2中
copy(slice2, slice1)

// 输出slice2中的元素
fmt.Println(slice2) // 输出:[2 3]

当 目标切片 的空间大小不足以包含 源切片 的所有值的时候,会损失超出的值

7)切片排序 可以使用sort包中的函数对切片进行排序,例如:

1
2
3
4
5
6
7
8
// 创建一个包含5个元素的整数切片
slice := []int{5, 2, 6, 3, 1}

// 对切片进行排序
sort.Ints(slice)

// 输出排序后的切片
fmt.Println(slice) // 输出:[1 2 3 5 6]

8)切片去重 可以使用map类型(后续讲解)实现切片去重,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 创建一个包含重复元素的整数切片
slice := []int{1, 2, 3, 2, 1}

// 创建一个空的map,用于存储不重复的元素
m := make(map[int]bool)

// 遍历切片中的元素,并将其存储到map中
for _, v := range slice {
    m[v] = true
}

// 将map中的元素存储到一个新的切片中
newSlice := []int{}
for k := range m {
    newSlice = append(newSlice, k)
}

// 输出去重后的切片
fmt.Println(newSlice) // 输出:[1 2 3]

2.2 切片(slice)的底层结构

切片是对数组一个连续片段的引用,所以切片是一个引用类型。

64位OS环境下,当创建切片的时候,使用sizeof计算切片变量大小只为24字节,原因就是它本质是一个结构体,存放着3个字段:

1
2
3
4
5
type slice struct {
   array unsafe.Pointer    // 底层数组头指针,指向 slice 可以访问到的第一个元素。
   len   int               // 当前切片长度,slice 中元素个数。
   cap   int               // 底层数组最大容量,slice 起始元素到底层数组最后一个元素间的元素个数。
}

使用 make 关键字创建一个长度为5的切片,如果没有指定cap参数则默认与长度一致为5(底层数组长度也为5),如下:

1
s := make([]byte,5)

直接通过已有 数组/切片 创建新切片,array指向底层数组的指定位置作为头部,从头部开始计算len及cap:

1
2
u := [10]byte{11,12,13,14,15,16,17,18,19,20} // 长度为10的数组
s := u[3:7] // 从数组的第4个元素至第7个元素建立切片(前闭后开区间)

底层数组可以被多个切片共享,如果切片共享了底层数组同一值,那么切片1进行修改后,切片2读取该值时也会读取到修改的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

package main

import "fmt"

func main() {
    u0 := [10]byte{11,12,13,14,15,16,17,18,19,20} // 长度为10的数组
    s1 := u0[1:8] // 从数组u0的第2个元素至第8个元素建立切片(前闭后开区间)
    s2 := u0[2:6] // 从数组u0的第3个元素至第6个元素建立切片(前闭后开区间)
    s3 := s1[1:5] // 从切片s1的第2个元素至第6个元素建立切片(前闭后开区间)

    fmt.Printf("u0 addr:%p, len:%v, cap:%v, val:%v\n",&u0, len(u0), len(u0), u0)
    fmt.Printf("s1 addr:%p, len:%v, cap:%v, val:%v\n", s1, len(s1), len(s1), s1)
    fmt.Printf("s2 addr:%p, len:%v, cap:%v, val:%v\n", s2, len(s2), len(s2), s2)
    fmt.Printf("s3 addr:%p, len:%v, cap:%v, val:%v\n\n", s3, len(s3), len(s3), s3)

    u0[3] -= 10
    s1[1] -= 10
    s2[2] -= 10
    s3[3] -= 10

    fmt.Println("After -= 10:")
    fmt.Printf("u0 addr:%p, len:%v, cap:%v, val:%v\n",&u0, len(u0), len(u0), u0)
    fmt.Printf("s1 addr:%p, len:%v, cap:%v, val:%v\n", s1, len(s1), len(s1), s1)
    fmt.Printf("s2 addr:%p, len:%v, cap:%v, val:%v\n", s2, len(s2), len(s2), s2)
    fmt.Printf("s3 addr:%p, len:%v, cap:%v, val:%v\n", s3, len(s3), len(s3), s3)
}
// 输出结果:
/*
u0 addr:0xc0000b2010, len:10, cap:10, val:[11 12 13 14 15 16 17 18 19 20]
s1 addr:0xc0000b2011, len:7, cap:7, val:[12 13 14 15 16 17 18]
s2 addr:0xc0000b2012, len:4, cap:4, val:[13 14 15 16]
s3 addr:0xc0000b2012, len:4, cap:4, val:[13 14 15 16]

After -= 10:
u0 addr:0xc0000b2010, len:10, cap:10, val:[11 12 3 4 5 6 17 18 19 20]
s1 addr:0xc0000b2011, len:7, cap:7, val:[12 3 4 5 6 17 18]
s2 addr:0xc0000b2012, len:4, cap:4, val:[3 4 5 6]
s3 addr:0xc0000b2012, len:4, cap:4, val:[3 4 5 6]

*/

2.3 切片(slice)的扩容机制

当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append 函数向切片中添加元素时。

slice 扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。

由于扩容会分配新数组并复制元素,因此频繁的扩容会非常影响性能。如果知道要添加多少元素,可以使用 make 函数预先分配足够大的切片来避免频繁扩容。

在 Go 语言的源码中,切片扩容通常是在进行切片的 append 操作时触发的。在进行 append 操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用 growslice 函数进行扩容。

growslice 函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行 append 操作时,编译器会将其转换为类似下面的代码:

1
slice = append(slice, elem)

在上述代码中,如果切片容量不足以容纳新的元素,则会调用 growslice 函数进行扩容。所以 growslice 函数的调用是由编译器在生成的机器码中实现的,而不是在源代码中显式调用的。

切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。

go1.18 之前版本切片扩容的 growslice 实现(go1.17)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/runtime/slice.go
growslice(et *_type, old slice, cap int) slice {
    // ...
    
    newcap := old.cap
    doublecap := newcap + newcap
    
    if cap = doublecap {
        newcap = cap
    } else {
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    // ...
    
    return slice{p, old.len, newcap}
}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于 1024 就会将容量翻倍;
  • 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

go1.18 版本开始切片扩容的 growslice 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/runtime/slice.go
growslice(et *_type, old slice, cap int) slice {
    // ...
    
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                // Transition from growing 2x for small slices
                // to growing 1.25x for large slices. This formula
                // gives a smooth-ish transition between the two.
                newcap += (newcap + 3*threshold) / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // ...
    
    return slice{p, old.len, newcap}
}

和之前版本的区别,主要在扩容阈值,以及这行代码:newcap += (newcap + 3*threshold) / 4

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  • 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;

切片扩容的内存对齐: 分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。

为什么呢? 实际上,growslice 的后半部分还有更进一步的优化(内存对齐等),靠的是 roundupsize 函数,在计算完 newcap 值之后,还会有一个步骤计算最终的容量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == goarch.PtrSize:
        lenmem = uintptr(old.len) * goarch.PtrSize
        newlenmem = uintptr(cap) * goarch.PtrSize
        capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
        newcap = int(capmem / goarch.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if goarch.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }

2.4 string和slice的关系

  • string 底层是一个byte数组,因此string也可以进行切片处理,
  • string 是不可变的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
 
import "fmt"

func main(){
    str := "hello@atguigu"  //string底层是一个byte数组,一昵称string也可以进行切片处理
    
    slice := str[6:]    // 使用切片获取到atguigu

    str[0] = 'z'   // 这样是不可用的,错的原因:string是不可变的

    fmt.Println(slice)
}

如果需要修改字符串,可以先将string 转换为 []byte 或者 []rune 修改后,重新转成字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main
 
import "fmt"
 
func main(){
    str := "hello@atguigu"
    slice := str[6:]
    fmt.Println(slice)
    
    arr1 := []byte(str)    // 如果修改字符串, 先转成byte数组
    arr1[0] = 'z'          // 修改
    str = string(arr1)     // 再转回字符串
    fmt.Println(str)
}

需要注意,转换成[]byte 后可以处理英文和数字,但是不能处理中文,原因是 []byte 是按字节来处理,而一个汉字是3个字节,因此就会出现乱码。

解决方法:将string转换为 []rune 即可,因为rune是兼容中文的,是按照字符进行处理的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main
 
import "fmt"

func main(){
    str := "hello@atguigu"
    slice := str[6:]
    fmt.Println(slice)
 
    // 修改为rune,添加的字符为 的
    arr1 := []rune(str)   
    arr1[0] = '的'     
    str = string(arr1)  
    fmt.Println(str)
}
//atguigu
//的ello@atguigu