I have been working on improving showing logs in the project I am working on. I did already clean some parts that grew out of their initial functions quickly and desperately needed some refactoring. One thing I noticed is that when I refactor complex pieces of code, there are some behaviours that are overseen and bugs appear. I wish it was only my inability to refactor code properly, but I do believe it is something we all face when wanting to simplify and organise. These bugs are a small price to pay in the long run. Unnecessarily complex code is so hard and time-consuming to debug.
With this in mind I tackled one issue regarding logs not being available for a certain type of object. At first, I tried and failed running the debugger on it. I managed to get some strange errors about strange objects – I have to sort my sample API calls out with our newest data structures. My second approach was the old spew.Dump(...)
(https://github.com/davecgh/go-spew) – which is a fancier print that I recommend. With this one I found out that the string is there at some point in time, but it disappears, and it disappears without the other fields disappearing.
I did arrive to something like:
1 2 3 4 | func PersistWithoutLog(object ObjectWithLog) error { object.LogHolder.Logs = "" ... } |
Which brings me to the issue I wanted to talk about: passing around structs with pointer fields.
When working with complex structs, and passing along an object by value, we need to be mindful about the other pointers this struct might contain. See this example code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package main import "fmt" type Book struct { page *Page } type Page struct { line string } func update(book Book) { book.page.line = "different-string" // some operation } func main() { book := Book{ page: &Page;{ line: "example-string" , }, } fmt.Printf( "%v\n" , book.page.line) update(book) fmt.Printf( "%v\n" , book.page.line) } |
Without running it in the Go playground, what would you expect to see printed out from the last fmt.Printf(...)
? Experienced developers can easily see in this example that, even though the object called book
was passed as value, the function update(...)
did indeed modify the underlying object. The output will be therefore "different string"
after the function was called.
Why did this happen? When a function is called with values instead of pointers, the attributes of the incoming object are copied over into a new object. The page
attribute is a pointer, what is stored in book
is its address, and what is copied over is this address as it is. Therefore, even if we have a new book
, it is still pointing to the same page
.
There are multiple solutions to this. One is to make sure whenever we want to use a copy of the book
, we create a deep copy of it, by manually making sure the pointers are copied as value:
1 2 3 4 5 6 7 8 | func deepCopy(book Book) Book { result := Book{} if book.page != nil { result.page = new(Page) *result.page = *book.page } return result } |
For really complex objects there’s also reflect.Copy(...)
and many other community-provided deep copy options.
When you have a more elegant solution or just one that you like better, definitely share it in the comments or as a direct message.
If you are stumbling into this issue quite often, I would also advise revising the structs you are using. In case the underlying pointer is not used separately or in other structs, it might make sense to not use pointers here. Passing around pointers can get hard to follow very soon. So unless there is a good reason to use them, maybe don’t.
What I really enjoy in Go is the simplicity and straightforwardness of the language. As long as you keep your code clean, it will be easy to maintain it and it will be easy to hunt down all the bugs that show up.