Movatterモバイル変換


[0]ホーム

URL:


The Go Blog

Arrays, slices (and strings): The mechanics of 'append'

Rob Pike
26 September 2013

Introduction

One of the most common features of procedural programming languages isthe concept of an array.Arrays seem like simple things but there are many questions that must beanswered when adding them to a language, such as:

  • fixed-size or variable-size?
  • is the size part of the type?
  • what do multidimensional arrays look like?
  • does the empty array have meaning?

The answers to these questions affect whether arrays are justa feature of the language or a core part of its design.

In the early development of Go, it took about a year to decide the answersto these questions before the design felt right.The key step was the introduction ofslices, which built on fixed-sizearrays to give a flexible, extensible data structure.To this day, however, programmers new to Go often stumble over the way sliceswork, perhaps because experience from other languages has colored their thinking.

In this post we’ll attempt to clear up the confusion.We’ll do so by building up the pieces to explain how theappend built-in functionworks, and why it works the way it does.

Arrays

Arrays are an important building block in Go, but like the foundation of a buildingthey are often hidden below more visible components.We must talk about them briefly before we move on to the more interesting,powerful, and prominent idea of slices.

Arrays are not often seen in Go programs becausethe size of an array is part of its type, which limits its expressive power.

The declaration

var buffer [256]byte

declares the variablebuffer, which holds 256 bytes.The type ofbuffer includes its size,[256]byte.An array with 512 bytes would be of the distinct type[512]byte.

The data associated with an array is just that: an array of elements.Schematically, our buffer looks like this in memory,

buffer: byte byte byte ... 256 times ... byte byte byte

That is, the variable holds 256 bytes of data and nothing else. We canaccess its elements with the familiar indexing syntax,buffer[0],buffer[1],and so on throughbuffer[255]. (The index range 0 through 255 covers256 elements.) Attempting to indexbuffer with a value outside thisrange will crash the program.

There is a built-in function calledlen that returns the number of elementsof an array or slice and also of a few other data types.For arrays, it’s obvious whatlen returns.In our example,len(buffer) returns the fixed value 256.

Arrays have their place—they are a good representation of a transformationmatrix for instance—but their most common purpose in Go is to hold storagefor a slice.

Slices: The slice header

Slices are where the action is, but to use them well one must understandexactly what they are and what they do.

A slice is a data structure describing a contiguous section of an arraystored separately from the slice variable itself.A slice is not an array.A slicedescribes a piece of an array.

Given ourbuffer array variable from the previous section, we could createa slice that describes elements 100 through 150 (to be precise, 100 through 149,inclusive) byslicing the array:

var slice []byte = buffer[100:150]

In that snippet we used the full variable declaration to be explicit.The variableslice has type[]byte, pronounced “slice of bytes”,and is initialized from the array, calledbuffer, by slicing elements 100 (inclusive) through 150 (exclusive).The more idiomatic syntax would drop the type, which is set by the initializing expression:

var slice = buffer[100:150]

Inside a function we could use the short declaration form,

slice := buffer[100:150]

What exactly is this slice variable?It’s not quite the full story, but for now think of aslice as a little data structure with two elements: a length and a pointer to an elementof an array.You can think of it as being built like this behind the scenes:

type sliceHeader struct {    Length        int    ZerothElement *byte}slice := sliceHeader{    Length:        50,    ZerothElement: &buffer[100],}

Of course, this is just an illustration.Despite what this snippet says thatsliceHeader struct is not visibleto the programmer, and the typeof the element pointer depends on the type of the elements,but this gives the general idea of the mechanics.

So far we’ve used a slice operation on an array, but we can also slice a slice, like this:

slice2 := slice[5:10]

Just as before, this operation creates a new slice, in this case with elements5 through 9 (inclusive) of the original slice, which means elements105 through 109 of the original array.The underlyingsliceHeader struct for theslice2 variable looks likethis:

slice2 := sliceHeader{    Length:        5,    ZerothElement: &buffer[105],}

Notice that this header still points to the same underlying array, stored inthebuffer variable.

We can alsoreslice, which is to say slice a slice and store the result back inthe original slice structure. After

slice = slice[5:10]

thesliceHeader structure for theslice variable looks just like it did for theslice2variable.You’ll see reslicing used often, for example to truncate a slice. This statement dropsthe first and last elements of our slice:

slice = slice[1:len(slice)-1]

[Exercise: Write out what thesliceHeader struct looks like after this assignment.]

You’ll often hear experienced Go programmers talk about the “slice header”because that really is what’s stored in a slice variable.For instance, when you call a function that takes a slice as an argument, such asbytes.IndexRune, that header iswhat gets passed to the function.In this call,

slashPos := bytes.IndexRune(slice, '/')

theslice argument that is passed to theIndexRune function is, in fact,a “slice header”.

There’s one more data item in the slice header, which we talk about below,but first let’s see what the existence of the slice header means when youprogram with slices.

Passing slices to functions

It’s important to understand that even though a slice contains a pointer,it is itself a value.Under the covers, it is a struct value holding a pointer and a length.It isnot a pointer to a struct.

This matters.

When we calledIndexRune in the previous example,it was passed acopy of the slice header.That behavior has important ramifications.

Consider this simple function:

func AddOneToEachElement(slice []byte) {    for i := range slice {        slice[i]++    }}

It does just what its name implies, iterating over the indices of a slice(using aforrange loop), incrementing its elements.

Try it:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")var buffer [256]bytevar slice []byte = buffer[100:150]func AddOneToEachElement(slice []byte) {    for i := range slice {        slice[i]++    }}
func main() {    slice := buffer[10:20]    for i := 0; i < len(slice); i++ {        slice[i] = byte(i)    }    fmt.Println("before", slice)    AddOneToEachElement(slice)    fmt.Println("after", slice)}

(You can edit and re-execute these runnable snippets if you want to explore.)

Even though the sliceheader is passed by value, the header includesa pointer to elements of an array, so both the original slice headerand the copy of the header passed to the function describe the samearray.Therefore, when the function returns, the modified elements canbe seen through the original slice variable.

The argument to the function really is a copy, as this example shows:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")var buffer [256]bytevar slice []byte = buffer[100:150]
func SubtractOneFromLength(slice []byte) []byte {    slice = slice[0 : len(slice)-1]    return slice}func main() {    fmt.Println("Before: len(slice) =", len(slice))    newSlice := SubtractOneFromLength(slice)    fmt.Println("After:  len(slice) =", len(slice))    fmt.Println("After:  len(newSlice) =", len(newSlice))}

Here we see that thecontents of a slice argument can be modified by a function,but itsheader cannot.The length stored in theslice variable is not modified by the call to the function,since the function is passed a copy of the slice header, not the original.Thus if we want to write a function that modifies the header, we must return it as a resultparameter, just as we have done here.Theslice variable is unchanged but the returned value has the new length,which is then stored innewSlice.

Pointers to slices: Method receivers

Another way to have a function modify the slice header is to pass a pointer to it.Here’s a variant of our previous example that does this:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")var buffer [256]bytevar slice []byte = buffer[100:150]
func PtrSubtractOneFromLength(slicePtr *[]byte) {    slice := *slicePtr    *slicePtr = slice[0 : len(slice)-1]}func main() {    fmt.Println("Before: len(slice) =", len(slice))    PtrSubtractOneFromLength(&slice)    fmt.Println("After:  len(slice) =", len(slice))}

It seems clumsy in that example, especially dealing with the extra level of indirection(a temporary variable helps),but there is one common case where you see pointers to slices.It is idiomatic to use a pointer receiver for a method that modifies a slice.

Let’s say we wanted to have a method on a slice that truncates it at the final slash.We could write it like this:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "bytes"    "fmt")
type path []bytefunc (p *path) TruncateAtFinalSlash() {    i := bytes.LastIndex(*p, []byte("/"))    if i >= 0 {        *p = (*p)[0:i]    }}func main() {    pathName := path("/usr/bin/tso") // Conversion from string to path.    pathName.TruncateAtFinalSlash()    fmt.Printf("%s\n", pathName)}

If you run this example you’ll see that it works properly, updating the slice in the caller.

[Exercise: Change the type of the receiver to be a value ratherthan a pointer and run it again. Explain what happens.]

On the other hand, if we wanted to write a method forpath that upper-casesthe ASCII letters in the path (parochially ignoring non-English names), the method couldbe a value because the value receiver will still point to the same underlying array.

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")
type path []bytefunc (p path) ToUpper() {    for i, b := range p {        if 'a' <= b && b <= 'z' {            p[i] = b + 'A' - 'a'        }    }}func main() {    pathName := path("/usr/bin/tso")    pathName.ToUpper()    fmt.Printf("%s\n", pathName)}

Here theToUpper method uses two variables in theforrange constructto capture the index and slice element.This form of loop avoids writingp[i] multiple times in the body.

[Exercise: Convert theToUpper method to use a pointer receiver and see if its behavior changes.]

[Advanced exercise: Convert theToUpper method to handle Unicode letters, not just ASCII.]

Capacity

Look at the following function that extends its argument slice ofints by one element:

func Extend(slice []int, element int) []int {    n := len(slice)    slice = slice[0 : n+1]    slice[n] = element    return slice}

(Why does it need to return the modified slice?) Now run it:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")func Extend(slice []int, element int) []int {    n := len(slice)    slice = slice[0 : n+1]    slice[n] = element    return slice}
func main() {    var iBuffer [10]int    slice := iBuffer[0:0]    for i := 0; i < 20; i++ {        slice = Extend(slice, i)        fmt.Println(slice)    }}

See how the slice grows until… it doesn’t.

It’s time to talk about the third component of the slice header: itscapacity.Besides the array pointer and length, the slice header also stores its capacity:

type sliceHeader struct {    Length        int    Capacity      int    ZerothElement *byte}

TheCapacity field records how much space the underlying array actually has; it is the maximumvalue theLength can reach.Trying to grow the slice beyond its capacity will step beyond the limits of the array and will trigger a panic.

After our example slice is created by

slice := iBuffer[0:0]

its header looks like this:

slice := sliceHeader{    Length:        0,    Capacity:      10,    ZerothElement: &iBuffer[0],}

TheCapacity field is equal to the length of the underlying array,minus the index in the array of the first element of the slice (zero in this case).If you want to inquire what the capacity is for a slice, use the built-in functioncap:

if cap(slice) == len(slice) {    fmt.Println("slice is full!")}

Make

What if we want to grow the slice beyond its capacity?You can’t!By definition, the capacity is the limit to growth.But you can achieve an equivalent result by allocating a new array, copying the data over, and modifyingthe slice to describe the new array.

Let’s start with allocation.We could use thenew built-in function to allocate a bigger arrayand then slice the result,but it is simpler to use themake built-in function instead.It allocates a new array andcreates a slice header to describe it, all at once.Themake function takes three arguments: the type of the slice, its initial length, and its capacity, which is thelength of the array thatmake allocates to hold the slice data.This call creates a slice of length 10 with room for 5 more (15-10), as you can see by running it:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")func main() {
    slice := make([]int, 10, 15)    fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
}

This snippet doubles the capacity of ourint slice but keeps its length the same:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")func main() {
    slice := make([]int, 10, 15)    fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))    newSlice := make([]int, len(slice), 2*cap(slice))    for i := range slice {        newSlice[i] = slice[i]    }    slice = newSlice    fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
}

After running this code the slice has much more room to grow before needing another reallocation.

When creating slices, it’s often true that the length and capacity will be same.Themake built-in has a shorthand for this common case.The length argument defaults to the capacity, so you can leave it outto set them both to the same value.After

gophers := make([]Gopher, 10)

thegophers slice has both its length and capacity set to 10.

Copy

When we doubled the capacity of our slice in the previous section,we wrote a loop to copy the old data to the new slice.Go has a built-in function,copy, to make this easier.Its arguments are two slices, and it copies the data from the right-hand argument to the left-hand argument.Here’s our example rewritten to usecopy:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")func main() {    slice := make([]int, 10, 15)    fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
    newSlice := make([]int, len(slice), 2*cap(slice))    copy(newSlice, slice)
   slice = newSlice    fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))}

Thecopy function is smart.It only copies what it can, paying attention to the lengths of both arguments.In other words, the number of elements it copies is the minimum of the lengths of the two slices.This can save a little bookkeeping.Also,copy returns an integer value, the number of elements it copied, although it’s not always worth checking.

Thecopy function also gets things right when source and destination overlap, which means it can be used to shiftitems around in a single slice.Here’s how to usecopy to insert a value into the middle of a slice.

// Insert inserts the value into the slice at the specified index,// which must be in range.// The slice must have room for the new element.func Insert(slice []int, index, value int) []int {// Grow the slice by one element.    slice = slice[0 : len(slice)+1]// Use copy to move the upper part of the slice out of the way and open a hole.    copy(slice[index+1:], slice[index:])// Store the new value.    slice[index] = value// Return the result.    return slice}

There are a couple of things to notice in this function.First, of course, it must return the updated slice because its length has changed.Second, it uses a convenient shorthand.The expression

slice[i:]

means exactly the same as

slice[i:len(slice)]

Also, although we haven’t used the trick yet, we can leave out the first element of a slice expression too;it defaults to zero. Thus

slice[:]

just means the slice itself, which is useful when slicing an array.This expression is the shortest way to say “a slice describing all the elements of the array”:

array[:]

Now that’s out of the way, let’s run ourInsert function.

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")// Insert inserts the value into the slice at the specified index,// which must be in range.// The slice must have room for the new element.func Insert(slice []int, index, value int) []int {    // Grow the slice by one element.    slice = slice[0 : len(slice)+1]    // Use copy to move the upper part of the slice out of the way and open a hole.    copy(slice[index+1:], slice[index:])    // Store the new value.    slice[index] = value    // Return the result.    return slice}func main() {
    slice := make([]int, 10, 20) // Note capacity > length: room to add element.    for i := range slice {        slice[i] = i    }    fmt.Println(slice)    slice = Insert(slice, 5, 99)    fmt.Println(slice)
}

Append: An example

A few sections back, we wrote anExtend function that extends a slice by one element.It was buggy, though, because if the slice’s capacity was too small, the function wouldcrash.(OurInsert example has the same problem.)Now we have the pieces in place to fix that, so let’s write a robust implementation ofExtend for integer slices.

func Extend(slice []int, element int) []int {    n := len(slice)    if n == cap(slice) {// Slice is full; must grow.// We double its size and add 1, so if the size is zero we still grow.        newSlice := make([]int, len(slice), 2*len(slice)+1)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[0 : n+1]    slice[n] = element    return slice}

In this case it’s especially important to return the slice, since when it reallocatesthe resulting slice describes a completely different array.Here’s a little snippet to demonstrate what happens as the slice fills up:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")// Extend extends the slice by adding the element to the end.func Extend(slice []int, element int) []int {    n := len(slice)    if n == cap(slice) {        // Slice is full; must grow.        // We double its size and add 1, so if the size is zero we still grow.        newSlice := make([]int, len(slice), 2*len(slice)+1)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[0 : n+1]    slice[n] = element    return slice}func main() {
    slice := make([]int, 0, 5)    for i := 0; i < 10; i++ {        slice = Extend(slice, i)        fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)        fmt.Println("address of 0th element:", &slice[0])    }
}

Notice the reallocation when the initial array of size 5 is filled up.Both the capacity and the address of the zeroth element change when the new array is allocated.

With the robustExtend function as a guide we can write an even nicer function that letsus extend the slice by multiple elements.To do this, we use Go’s ability to turn a list of function arguments into a slice when thefunction is called.That is, we use Go’s variadic function facility.

Let’s call the functionAppend.For the first version, we can just callExtend repeatedly so the mechanism of the variadic function is clear.The signature ofAppend is this:

func Append(slice []int, items ...int) []int

What that says is thatAppend takes one argument, a slice, followed by zero or moreint arguments.Those arguments are exactly a slice ofint as far as the implementationofAppend is concerned, as you can see:

// Append appends the items to the slice.// First version: just loop calling Extend.func Append(slice []int, items ...int) []int {    for _, item := range items {        slice = Extend(slice, item)    }    return slice}

Notice theforrange loop iterating over the elements of theitems argument, which has implied type[]int.Also notice the use of the blank identifier_ to discard the index in the loop, which we don’t need in this case.

Try it:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")// Extend extends the slice by adding the element to the end.func Extend(slice []int, element int) []int {    n := len(slice)    if n == cap(slice) {        // Slice is full; must grow.        // We double its size and add 1, so if the size is zero we still grow.        newSlice := make([]int, len(slice), 2*len(slice)+1)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[0 : n+1]    slice[n] = element    return slice}// Append appends the items to the slice.// First version: just loop calling Extend.func Append(slice []int, items ...int) []int {    for _, item := range items {        slice = Extend(slice, item)    }    return slice}func main() {
    slice := []int{0, 1, 2, 3, 4}    fmt.Println(slice)    slice = Append(slice, 5, 6, 7, 8)    fmt.Println(slice)
}

Another new technique in this example is that we initialize the slice by writing a composite literal,which consists of the type of the slice followed by its elements in braces:

    slice := []int{0, 1, 2, 3, 4}

TheAppend function is interesting for another reason.Not only can we append elements, we can append a whole second sliceby “exploding” the slice into arguments using the... notation at the call site:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")// Extend extends the slice by adding the element to the end.func Extend(slice []int, element int) []int {    n := len(slice)    if n == cap(slice) {        // Slice is full; must grow.        // We double its size and add 1, so if the size is zero we still grow.        newSlice := make([]int, len(slice), 2*len(slice)+1)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[0 : n+1]    slice[n] = element    return slice}// Append appends the items to the slice.// First version: just loop calling Extend.func Append(slice []int, items ...int) []int {    for _, item := range items {        slice = Extend(slice, item)    }    return slice}func main() {
    slice1 := []int{0, 1, 2, 3, 4}    slice2 := []int{55, 66, 77}    fmt.Println(slice1)    slice1 = Append(slice1, slice2...) // The '...' is essential!    fmt.Println(slice1)
}

Of course, we can makeAppend more efficient by allocating no more than once,building on the innards ofExtend:

// Append appends the elements to the slice.// Efficient version.func Append(slice []int, elements ...int) []int {    n := len(slice)    total := len(slice) + len(elements)    if total > cap(slice) {// Reallocate. Grow to 1.5 times the new size, so we can still grow.        newSize := total*3/2 + 1        newSlice := make([]int, total, newSize)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[:total]    copy(slice[n:], elements)    return slice}

Here, notice how we usecopy twice, once to move the slice data to the newlyallocated memory, and then to copy the appending items to the end of the old data.

Try it; the behavior is the same as before:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")// Append appends the elements to the slice.// Efficient version.func Append(slice []int, elements ...int) []int {    n := len(slice)    total := len(slice) + len(elements)    if total > cap(slice) {        // Reallocate. Grow to 1.5 times the new size, so we can still grow.        newSize := total*3/2 + 1        newSlice := make([]int, total, newSize)        copy(newSlice, slice)        slice = newSlice    }    slice = slice[:total]    copy(slice[n:], elements)    return slice}func main() {
    slice1 := []int{0, 1, 2, 3, 4}    slice2 := []int{55, 66, 77}    fmt.Println(slice1)    slice1 = Append(slice1, slice2...) // The '...' is essential!    fmt.Println(slice1)
}

Append: The built-in function

And so we arrive at the motivation for the design of theappend built-in function.It does exactly what ourAppend example does, with equivalent efficiency, but itworks for any slice type.

A weakness of Go is that any generic-type operations must be provided by therun-time. Some day that may change, but for now, to make working with sliceseasier, Go provides a built-in genericappend function.It works the same as ourint slice version, but forany slice type.

Remember, since the slice header is always updated by a call toappend, you needto save the returned slice after the call.In fact, the compiler won’t let you call append without saving the result.

Here are some one-liners intermingled with print statements. Try them, edit them and explore:

// Copyright 2013 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.package mainimport (    "fmt")func main() {
    // Create a couple of starter slices.    slice := []int{1, 2, 3}    slice2 := []int{55, 66, 77}    fmt.Println("Start slice: ", slice)    fmt.Println("Start slice2:", slice2)    // Add an item to a slice.    slice = append(slice, 4)    fmt.Println("Add one item:", slice)    // Add one slice to another.    slice = append(slice, slice2...)    fmt.Println("Add one slice:", slice)    // Make a copy of a slice (of int).    slice3 := append([]int(nil), slice...)    fmt.Println("Copy a slice:", slice3)    // Copy a slice to the end of itself.    fmt.Println("Before append to self:", slice)    slice = append(slice, slice...)    fmt.Println("After append to self:", slice)
}

It’s worth taking a moment to think about the final one-liner of that example in detail to understandhow the design of slices makes it possible for this simple call to work correctly.

There are lots more examples ofappend,copy, and other ways to use sliceson the community-built“Slice Tricks” Wiki page.

Nil

As an aside, with our newfound knowledge we can see what the representation of anil slice is.Naturally, it is the zero value of the slice header:

sliceHeader{    Length:        0,    Capacity:      0,    ZerothElement: nil,}

or just

sliceHeader{}

The key detail is that the element pointer isnil too. The slice created by

array[0:0]

has length zero (and maybe even capacity zero) but its pointer is notnil, soit is not a nil slice.

As should be clear, an empty slice can grow (assuming it has non-zero capacity), but anilslice has no array to put values in and can never grow to hold even one element.

That said, anil slice is functionally equivalent to a zero-length slice, even though it pointsto nothing.It has length zero and can be appended to, with allocation.As an example, look at the one-liner above that copies a slice by appendingto anil slice.

Strings

Now a brief section about strings in Go in the context of slices.

Strings are actually very simple: they are just read-only slices of bytes with a bitof extra syntactic support from the language.

Because they are read-only, there is no need for a capacity (you can’t grow them),but otherwise for most purposes you can treat them just like read-only slicesof bytes.

For starters, we can index them to access individual bytes:

slash := "/usr/ken"[0] // yields the byte value '/'.

We can slice a string to grab a substring:

usr := "/usr/ken"[0:4] // yields the string "/usr"

It should be obvious now what’s going on behind the scenes when we slice a string.

We can also take a normal slice of bytes and create a string from it with the simple conversion:

str := string(slice)

and go in the reverse direction as well:

slice := []byte(usr)

The array underlying a string is hidden from view; there is no way to access its contentsexcept through the string. That means that when we do either of these conversions, acopy of the array must be made.Go takes care of this, of course, so you don’t have to.After either of these conversions, modifications tothe array underlying the byte slice don’t affect the corresponding string.

An important consequence of this slice-like design for strings is thatcreating a substring is very efficient.All that needs to happenis the creation of a two-word string header. Since the string is read-only, the originalstring and the string resulting from the slice operation can share the same array safely.

A historical note: The earliest implementation of strings always allocated, but when sliceswere added to the language, they provided a model for efficient string handling. Some ofthe benchmarks saw huge speedups as a result.

There’s much more to strings, of course, and aseparate blog post covers them in greater depth.

Conclusion

To understand how slices work, it helps to understand how they are implemented.There is a little data structure, the slice header, that is the item associated with the slicevariable, and that header describes a section of a separately allocated array.When we pass slice values around, the header gets copied but the array it pointsto is always shared.

Once you appreciate how they work, slices become not only easy to use, butpowerful and expressive, especially with the help of thecopy andappendbuilt-in functions.

More reading

There’s lots to find around the intertubes about slices in Go.As mentioned earlier,the“Slice Tricks” Wiki pagehas many examples.TheGo Slices blog postdescribes the memory layout details with clear diagrams.Russ Cox’sGo Data Structures article includesa discussion of slices along with some of Go’s other internal data structures.

There is much more material available, but the best way to learn about slices is to use them.

Next article:Strings, bytes, runes and characters in Go
Previous article:The first Go program
Blog Index

go.dev uses cookies from Google to deliver and enhance the quality of its services and to analyze traffic.Learn more.

[8]ページ先頭

©2009-2025 Movatter.jp