Search code examples
goslicegoroutine

Golang slice append and reallocation


I've been learning go recently and had a question about the behavior of slices when reallocation occurs. Assume I have a slice of pointers to a struct, such as:

var a []*A

If I were to pass this slice to another function, and my understanding is that internally this passes a slice header by value, that runs on a separate goroutine and just reads from the slice, while the function that launched the goroutine continues to append to the slice, is that a problem? For example:

package main

type A struct {
    foo int
}

func main() {
  a := make([]*A, 0, 100)
  ch := make(chan int)
  for i := 0; i < 100; i++ {
    a = append(a, &A{i})
  }
  go read_slice(a, ch)
  for i := 0; i < 100; i++ {
    a = append(a, &A{i+100})
  }
  <-ch
}

func read_slice(a []*A, ch chan int) {
   for i := range a {
     fmt.Printf("%d   ", a[i].foo)
   }
   ch <- 1
}

So from my understanding, as the read_slice() function is running on its own goroutine with a copy of the slice header, it has an underlying pointer to the current backing array and the size at the time it was called through which I can access the foo's.

However, when the other goroutine is appending to the slice it will trigger a reallocation when the capacity is exceeded. Does the go runtime not deallocate the memory to the old backing array being used in read_slice() since there is a reference to it in that function?

I tried running this with "go run -race slice.go" but that didn't report anything, but I feel like I might be doing something wrong here? Any pointers would be appreciated.

Thanks!


Solution

  • The GC does not collect the backing array until there are no references to the backing array. There are no races in the program.

    Consider the scenario with no goroutines:

      a := make([]*A, 0, 100)
      for i := 0; i < 100; i++ {
        a = append(a, &A{i})
      }
      b := a
      for i := 0; i < 100; i++ {
        b = append(b, &A{i+100})
      }
    

    The slice a will continue to reference the backing array with the first 100 pointers when append to b allocates a new backing array. The slice a is not left with a dangling reference to a backing array.

    Now add the goroutine to the scenario:

      a := make([]*A, 0, 100)
      for i := 0; i < 100; i++ {
        a = append(a, &A{i})
      }
      b := a
      go read_slice(a, ch)
      for i := 0; i < 100; i++ {
        b = append(b, &A{i+100})
      }
    

    The goroutine can happily use slice a. There's no dangling reference.

    Now consider the program in the question. It's functionaly identical to the last snippet here.