[The Go Programming Language] 4. Composite Types

4. Composite Types

Composite types, the molecules created by combining the basic types in various ways. We’ll talk about four such types—arrays, slices, maps, and structs.

Arrays and structs are aggregate types; their values are concatenations of other values in memory. Arrays are homogeneous—their elements all have the same type—whereas structs are heterogeneous. Both arrays and structs are fixed size. In contrast, slices and maps are dynamic data structures that grow as values are added.

4.1 Arrays, fixed size and fixed types

An array is a fixed-length sequence of zero or more elements of a particular type. Because of their fixed length, arrays are rarely used directly in Go. Slices, which can grow and shrink, are much more versatile.

Individual array elements are accessed with the conventional subscript notation, where subscripts run from zero to one less than the array length. The built-in function len returns the number of elements in the array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a [3]int             // array of 3 integers, 0
fmt.Println(a[0]) // print the first element, 0
fmt.Println(a[len(a)-1]) // print the last element, a[2], 0

// Print the indices and elements.
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
// 0 0
// 1 0
// 2 0
}

// Print the elements only.
for _, v := range a {
fmt.Printf("%d\n", v)
// 0
// 0
// 0
}

By default, the elements of a new array variable are initially set to the zero value for the element type, which is 0 for numbers. We can use an array literal to initialize an array with a list of values:

1
2
3
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0”

Array Literal

In an array literal, if an ellipsis “…” appears in place of the length, the array length is determined by the number of initializers. The definition of q can be simplified to

1
2
q := [...]int{1, 2, 3} // array literal
fmt.Printf("%T\n", q) // "[3]int"

The size of an array is part of its type, so [3]int and [4]int are different types. The size must be a constant expression, that is, an expression whose value can be computed as the program is being compiled.

1
2
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int”

As we’ll see, the literal syntax is similar for arrays, slices, maps, and structs. The specific form above is a list of values in order, but it is also possible to specify a list of index and value pairs, like this:

1
2
3
4
5
6
7
8
9
10
11
12
type Currency int

const (
USD Currency = iota
EUR
GBP
RMB
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

In this form, indices can appear in any order and some may be omitted; as before, unspecified values take on the zero value for the element type. For instance,

1
r := [...]int{99: -1}

defines an array r with 100 elements, all zero except for the last, which has value −1.

If an array’s element type is comparable then the array type is comparable too, so we may directly compare two arrays of that type using the == operator, which reports whether all corresponding elements are equal. The != operator is its negation.

1
2
3
4
5
6
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int

When a function is called, a copy of each argument value is assigned to the corresponding parameter variable, so the function receives a copy, not the original. Passing large arrays in this way can be inefficient, and any changes that the function makes to array elements affect only the copy, not the original. In this regard, Go treats arrays like any other type, but this behavior is different from languages that implicitly pass arrays by reference.

Of course, we can explicitly pass a pointer to an array so that any modifications the function makes to array elements will be visible to the caller. This function zeroes the contents of a [32]byte array:

1
2
3
4
5
func zero(ptr *[32]byte) {
for i := range ptr {
ptr[i] = 0
}
}

Using a pointer to an array is efficient and allows the called function to mutate the caller’s variable, but arrays are still inherently inflexible because of their fixed size. The zero function will not accept a pointer to a [16]byte variable, for example, nor is there any way to add or remove array elements. For these reasons, other than special cases like SHA256’s fixed-size hash, arrays are seldom used as function parameters; instead, we use slices.

References

[1] The Go Programming Language - http://www.gopl.io/

[2] Arrays | A Tour of Go - https://tour.golang.org/moretypes/6

[3] Go by Example: Arrays - https://gobyexample.com/arrays