The slice type

The slice type is commonly used as the idiomatic construct for indexed data in Go. The slice is more flexible and has many more interesting characteristics than arrays. The slice itself is a composite type with semantics similar to arrays. In fact, a slice uses an array as its underlying data storage mechanism. The general form of a slice type is given as follows:

[ ]<element_type>

The one obvious difference between a slice and an array type is omission of the size in the type declaration, as shown in the following examples:

var ( 
    image []byte      
    ids []string 
    vector []float64 
    months []string 
    q1 []string 
    histogram []map[string]int // slice of map (see map later) 
) 

golang.fyi/ch07/slicetypes.go

The missing size attribute in the slice type indicates the following:

  • Unlike arrays, the size of a slice is not fixed
  • A slice type represents all sets of the specified element type

This means a slice can theoretically grow unbounded (though in practice this is not true as the slice is backed by an underlying bounded array). A slice of a given element type is considered to be the same type regardless of its underlying size. This removes the restriction found in arrays where the size determines the type.

For instance, the following variables, months and q1, have the same type of []string and will compile with no problem:

var ( 
    months []string 
    q1 []string 
) 
func print(strs []string){ ... } 
func main() { 
   print(months) 
   print(q1) 
} 

golang.fyi/ch07/slicetypes.go

Similar to arrays, slice types may be nested to create multi-dimensional slices, as shown in the following code snippet. Each dimension can independently have its own size and must be initialized individually:

var( 
    board [][]int 
    graph [][][][]int 
) 

Slice initialization

A slice is represented by the type system as a value (the next section explores the internal representation of a slice). However, unlike the array type, an uninitialized slice has a zero value of nil, which means any attempt to access elements of an uninitialized slice will cause a program to panic.

One of the simplest ways to initialize a slice is with a composite literal value using the following format (similar to an array):

<slice_type>{<comma-separated list of element values>}

The literal value for a slice is composed of the slice type followed by a set of comma-separated values, enclosed in curly brackets, that are assigned to the elements of the slice. The following code snippet illustrates several slice variables initialized with composite literal values:

var ( 
    ids []string = []string{"fe225", "ac144", "3b12c"} 
    vector = []float64{12.4, 44, 126, 2, 11.5}  
    months = []string { 
         "Jan", "Feb", "Mar", "Apr", 
         "May", "Jun", "Jul", "Aug", 
         "Sep", "Oct", "Nov", "Dec", 
    } 
    // slice of map type (maps are covered later) 
    tables = []map[string][]int { 
         { 
               "age":{53, 13, 5, 55, 45, 62, 34, 7}, 
               "pay":{124, 66, 777, 531, 933, 231}, 
         }, 
    } 
    graph  = [][][][]int{ 
         {{{44}, {3, 5}}, {{55, 12, 3}, {22, 4}}}, 
         {{{22, 12, 9, 19}, {7, 9}}, {{43, 0, 44, 12}, {7}}},     
    } 
) 

golang.fyi/ch07/sliceinit.go

As mentioned, the composite literal value of a slice is expressed using a similar form as the array. However, the number of elements provided in the literal is not bounded by a fixed size. This implies that the literal can be as large as needed. Under the cover though, Go creates and manages an array of appropriate size to store the values expressed in the literal.

Slice representation

Earlier it was mentioned that the slice value uses an underlying array to store data. The name slice, in fact, is a reference to a slice of data segment from the array. Internally, a slice is represented by a composite value with the followings three attributes:

Attribute

Description

a pointer

The pointer is the address of the first element of the slice stored in an underlying array. When the slice value is uninitialized, its pointer value is nil, indicating that it is not pointing to an array yet.

Go uses the pointer as the zero value of the slice itself. An uninitialized slice will return nil as its zero value. However, the slice value is not treated as a reference value by the type system. This means certain functions can be applied to a nil slice while others will cause a panic.

Once a slice is created, the pointer does not change. To point to a different starting point, a new slice must be created.

a length

The length indicates the number of contiguous elements that can be accessed starting with the first element. It is a dynamic value that can grow up to the capacity of the slice (see capacity next).

The length of a slice is always less than or equal to its capacity. Attempts to access elements beyond the length of a slice, without resizing, will result in a panic. This is true even when the capacity is larger than the length.

a capacity

The capacity of a slice is the maximum number of elements that may be stored in the slice, starting from its first element. The capacity of a slice is bounded by the length of the underlying array.

So, when the following variable halfyr is initialized as shown:

halfyr := []string{"Jan","Feb","Mar","Apr","May","Jun"}

It will be stored in an array of type [6]string with a pointer to the first element, a length, and a capacity of 6, as represented graphically in the following figure:

Slice representation

Slicing

Another way to create a slice value is by slicing an existing array or another slice value (or pointers to these values). Go provides an indexing format that makes it easy to express the slicing operation, as follows:

<slice or array value>[<low_index>:<high_index>]

The slicing expression uses the [:] operator to specify the low and high bound indices, separated by a colon, for the slice segment.

  • The low value is the zero-based index where the slice segment starts
  • The high value is the nth element offset where the segment stops

The following table shows examples of slice expressions by re-slicing the following value: halfyr := []string{"Jan","Feb","Mar","Apr","May","Jun"}.

Expression

Description

all := halfyr[:]

Omitting the low and high indices in the expression is equivalent to the following:

all := halfyr[0 : 6]

This produces a new slice segment equal to the original, which starts at index position 0 and stops at offset position 6:

["Jan","Feb","Mar","Apr","May","Jun"]

q1 := halfyr[:3]

Here the slice expression omits low index value and specifies a slice segment length of 3. It returns new slice, ["Jan","Feb","Mar"].

q2 := halfyr[3:]

This creates a new slice segment with the last three elements by specifying the staring index position of 3 and omitting the high bound index value, which defaults to 6.

mapr := halfyr[2:4]

To clear any confusion about slicing expressions, this example shows how to create a new slice with the months "Mar" and "Apr". This returns a slice with the value ["Mar","Apr"].

Slicing a slice

Slicing an existing slice or array value does not create a new underlying array. The new slice creates new pointer location to the underlying array. For instance, the following code shows the slicing of the slice value halfyr into two additional slices:

var ( 
    halfyr = []string{ 
         "Jan", "Feb", "Mar", 
         "Apr", "May", "Jun", 
    } 
 
    q1 = halfyr[:3] 
    q2 = halfyr[3:] 
) 

golang.fyi/ch07/slice_reslice.go

The backing array may have many slices projecting a particular view of its data. The following figure illustrates how slicing in the previous code may be represented visually:

Slicing a slice

Notice that both slices q1 and q2 are pointing to different elements in the same underlying array. Slice q1 has an initial length of 3 with a capacity of 6. This implies q1 can be resized up to 6 elements in total. Slice q2, however, has a size of 3 and a capacity of 3 and cannot grow beyond its initial size (slice resizing is covered later).

Slicing an array

As mentioned, an array can also be sliced directly. When that is the case, the provided array value becomes the underlying array. The capacity and the length the slices will be calculated using the provided array. The following source snippet shows the slicing of an existing array value called months:

var ( 
    months [12]string = [12]string{ 
         "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 
    } 
 
    halfyr = months[:6] 
    q1 = halfyr[:3] 
    q2 = halfyr[3:6] 
    q3 = months[6:9] 
    q4 = months[9:] 
) 

golang.fyi/ch07/slice_reslice_arr.go

Slice expressions with capacity

Lastly, Go's slice expression supports a longer form where the maximum capacity of the slice is included in the expression, as shown here:

<slice_or_array_value>[<low_index>:<high_index>:max]

The max attribute specifies the index value to be used as the maximum capacity of the new slice. That value may be less than, or equal to, the actual capacity of the underlying array. The following example slices an array with the max value included:

var ( 
    months [12]string = [12]string{ 
         "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 
    } 
    summer1 = months[6:9:9] 
) 

golang.fyi/ch07/slice_reslice_arr.go

The previous code snippet creates a new slice value summer1 with size 3 (starting at index position 6 to 9). The max index is set to position 9, which means the slice has a capacity of 3. If the max was not specified, the maximum capacity would automatically be set to the last position of the underlying array as before.

Making a slice

A slice can be initialized at runtime using the built-in function make. This function creates a new slice value and initializes its elements with the zero value of the element type. An uninitialized slice has a nil zero value an indication that it is not pointing an underlying array. Without an explicitly initialization, with a composite literal value or using the make() function, attempts to access elements of a slice will cause a panic. The following snippet reworks the previous example to use the make() function to initialize the slice:

func main() { 
   months := make([]string, 6) 
   ... 
} 

golang.fyi/ch07/slicemake.go

The make() function takes as an argument the type of the slice to be initialized and an initial size for the slice. Then it returns a slice value. In the previous snippet, make() does the followings:

  • Creates an underlying array of type [6]string
  • Creates the slice value with length and capacity of 6
  • Returns a slice value (not a pointer)

After initialization with the make() function, access to a legal index position will return the zero value for the slice element instead of causing a program panic. The make() function can take an optional third parameter that specifies the maximum capacity of the slice, as shown in the following example:

func main() { 
   months := make([]string, 6, 12)  
   ... 
} 

golang.fyi/ch07/slicemake2.go

The preceding snippet will initialize the months variable with a slice value with an initial length of 6 and a maximum capacity of 12.

Using slices

The simplest operation to do with a slice value is to access its elements. As was mentioned, slices use index notation to access its elements similar to arrays. The following example accesses element at index position 0 and updates to 15:

func main () { 
   h := []float64{12.5, 18.4, 7.0} 
   h[0] = 15 
   fmt.Println(h[0]) 
   ... 
} 

golang.fyi/ch07/slice_use.go

When the program runs, it prints the updated value using index expression h[0] to retrieve the value of the item at position 0. Note that the slice expression with only the index number, h[0] for instance, returns the value of the item at that position. When, however, the expression includes a colon, say h[2:] or h[:6], that expression returns a new slice.

Slice traversal can be done using the traditional forstatement or with the, more idiomatic, for…range statement as shown in the following code snippets:

func scale(factor float64, vector []float64) []float64 { 
   for i := range vector { 
         vector[i] *= factor 
   } 
   return vector 
} 
 
func contains(val float64, numbers []float64) bool { 
   for _, num := range numbers { 
         if num == val { 
               return true 
         } 
   } 
   return false 
} 

golang.fyi/ch07/slice_loop.go

In the previous code snippet, function scale uses index variable i to update the values in slice factor directly, while function contains uses the iteration-emitted value stored in num to access the slice element. If you need further detail on the for…range statement, see Chapter 3, Go Control Flow.

Slices as parameters

When a function receives a slice as its parameter, the internal pointer of that slice points to the underlying array of the slice. Therefore, all updates to the slice, within the function, will be seen by the function's caller. For instance, in the following code snippet, all changes to the vector parameter will be seen by the caller of function scale:

func scale(factor float64, vector []float64) { 
   for i := range vector { 
         vector[i] *= factor 
   } 
} 

golang.fyi/ch07/slice_loop.go

Length and capacity

Go provides two built-in functions to query the length and capacity attributes of a slice. Given a slice, its length and maximum capacity can be queried, using the len and cap functions respectively, as shown in the following example:

func main() { 
    var vector []float64 
    fmt.Println(len(vector)) // prints 0, no panic 
    h := make([]float64, 4, 10) 
    fmt.Println(len(h), ",", cap(h)) 
} 

Recall that a slice is a value (not a pointer) that has a nil as its zero-value. Therefore, the code is able to query the length (and capacity) of an uninitialized slice without causing a panic at runtime.

Appending to slices

The one indispensable feature of slice types is their ability to dynamically grow. By default, a slice has a static length and capacity. Any attempt to access an index beyond that limit will cause a panic. Go makes available the built-in variadic function append to dynamically add new values to a specified slice, growing its lengths and capacity, as necessary. The following code snippet shows how that is done:

func main() { 
   months := make([]string, 3, 3) 
   months = append(months, "Jan", "Feb", "March",  
    "Apr", "May", "June") 
   months = append(months, []string{"Jul", "Aug", "Sep"}...) 
   months = append(months, "Oct", "Nov", "Dec") 
   fmt.Println(len(months), cap(months), months) 
} 

golang.fyi/ch07/slice_append.go

The previous snippet starts with a slice with a size and capacity of 3. The append function is used to dynamically add new values to the slice beyond its initial size and capacity. Internally, append will attempt to fit the appended values within the target slice. If the slice has not been initialized or has an inadequate capacity, append will allocate a new underlying array, to store the values of the updated slice.

Copying slices

Recall that assigning or slicing an existing slice value simply creates a new slice value pointing to the same underlying array structure. Go offers the copy function, which returns a deep copy of the slice along with a new underlying array. The following snippet shows a clone() function, which makes a new copy of a slice of numbers:

func clone(v []float64) (result []float64) { 
   result = make([]float64, len(v), cap(v)) 
   copy(result, v) 
   return 
} 

golang.fyi/ch07/slice_use.go

In the previous snippet, the copy function copies the content of v slice into result. Both source and target slices must be the same size and of the same type or the copy operation will fail.

Strings as slices

Internally, the string type is implemented as a slice using a composite value that points to an underlying array of rune. This affords the string type the same idiomatic treatment given to slices. For instance, the following code snippet uses index expressions to extract slices of strings from a given string value:

func main() { 
   msg := "Bobsayshelloworld!" 
   fmt.Println( 
         msg[:3], msg[3:7], msg[7:12],  
         msg[12:17], msg[len(msg)-1:], 
   ) 
} 

golang.fyi/ch07/slice_string.go

The slice expression on a string will return a new string value pointing to its underlying array of runes. The string values can be converted to a slice of byte (or slice of rune) as shown in the following function snippet, which sorts the characters of a given string:

func sort(str string) string { 
   bytes := []byte(str) 
   var temp byte 
   for i := range bytes { 
         for j := i + 1; j < len(bytes); j++ { 
               if bytes[j] < bytes[i] { 
                     temp = bytes[i] 
                     bytes[i], bytes[j] = bytes[j], temp 
               } 
         } 
   } 
   return string(bytes) 
} 

golang.fyi/ch07/slice_string.go

The previous code shows the explicit conversion of a slice of bytes to a string value. Note that each character may be accessed using the index expression.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.229.111