others-how to modify an element in slice when developing golang programs ?

1. Purpose

In this post, I would demo how to modify elements in a slice or array in golang using range operator.

2. Environment

  • go version go1.14 darwin/amd64

3. The code

3.1 The slice introduction

We all know that slice is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays.

We can define slice like this:

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
}

Here we declare one variable named primes, which is an array, then we declared a slice of that array named s. You can see that the slice is just a view of the array, from index 1 to index 4, after print, we get this result:

[3 5 7]

3.2 The Book structure, array and initialization function

Now if we have a structure named Book ,which represents a book in reality:

type Book struct {
	ID     uint
	Title  string
	Author string
}

And for initialization, we define a global variable and an init function:

var Books []Book

func init() {
	Books = []Book {
		Book{
			ID: 1,
			Title: "aaaaaa",
			Author: "jack",
		},
		Book{
			ID: 2,
			Title: "bbbbbb",
			Author: "mike",
		},
	}
}

What is init function?

In Go, the init() function is incredibly powerful and compared to some other languages, is a lot easier to use within your Go programs. These init() functions can be used within a package block and regardless of how many times that package is imported, the init() function will only be called once.

We don’t need to call the init() function explicitly within our program. Go will handle the execution for us implicitly.

3.3 The PrintBooks function

We need a function to print all the books, the code is as follows:

func PrintBooks() {
	for _,book:=range Books {
		fmt.Printf("%v\n",book)
	}
}

What is ‘%v’ when doing print?

If the value is a struct, the %+v variant will include the struct’s field names.

fmt.Printf("%+v\n", p)

The %#v variant prints a Go syntax representation of the value, i.e. the source code snippet that would produce that value.

3.4 The function modify the element in slice that does NOT work

Here is a function that does not work, it iterates over the slice and modify the book that matches, here is the code:

//not work
func ChangeABook1(bookId string,newTitle string)(bool,error) {
	defaultResult := false
	for _,book:=range Books { // iterate over the Books slice
		if idstring := fmt.Sprint(book.ID); idstring==bookId { // find the matched book
			book.Title = newTitle //modify the matched book's Title
			return true,nil
		}
	}
	return defaultResult,errors.New("not found "+bookId)
}

As the function name implies, it would try to find a book that matches the specified bookId and change its Title to newTitle.

3.5 The main fucntion

func main() {
	fmt.Print("before change\n")
	fmt.Print("===============\n")
	PrintBooks()
	ChangeABook1("1","newnewnewnnew")
	fmt.Print("\nafter change\n")
	fmt.Print("===============\n")
	PrintBooks()
}

Now let’s run the code, we got this:

before change
===============
{1 aaaaaa jack}
{2 bbbbbb mike}

after change
===============
{1 aaaaaa jack}
{2 bbbbbb mike}

You can see that the modification or change does not work. The books are not changed at all, why did this happen?

4. Reason

The range loop copies the values from the slice to local variables; updating local variables will not affect the slice. Just as the picture shows:

image-20210309151300443

  • When you call range on the Books, it copies all the elements’ value of the original Books slice to a new slice
  • When you modify the Book, you are actually modifying the Book in the new slice, the original Books not modified at all

5. Solution

We should use the index to reference the original Books slice when modifying the book, just as follows:

func ChangeABook2(bookId string,newTitle string)(bool,error) {
	defaultResult := false
	for idx,book:=range Books { // here we want to use the idx variable of the range result
		if idstring := fmt.Sprint(book.ID); idstring==bookId {
			Books[idx].Title = newTitle  // here is the key point
			return true,nil
		}
	}
	return defaultResult,errors.New("not found "+bookId)
}

The difference from the first one is as follows:

  • This function use the ‘idx’ variable from the range result, which indicates the index of every book in the slice
  • This function use the Books[idx].Title = newTitle to reference the original slice

Let’s change and re-run our main code:

func main() {
	fmt.Print("before change\n")
	fmt.Print("===============\n")
	PrintBooks()
	ChangeABook2("1","newnewnewnnew") // here we use the ChangeABook2 function
	fmt.Print("\nafter change\n")
	fmt.Print("===============\n")
	PrintBooks()
}

we got this:

before change
===============
{1 aaaaaa jack}
{2 bbbbbb mike}

after change
===============
{1 newnewnewnnew jack}
{2 bbbbbb mike}

It works!

6. Summary

In this post, we demonstrated how to change or modify the element of a slice when using range operator to iterate over a slice. The key point you should mind is that go would copy values of the original slice to a new one when using range operator, this is a trap. Be careful!

The whole demo code is uploaded as a gist in github.com, you can get it here.