Pass By Value
Golang is pass by value. There is no pass by reference. It will allocate a new variable to hold every parameter passed to a function. Golang does however have pointers. And you can pass pointers to a function. In practice it works pretty much the same as pass by reference in C++. Inside the called function you have a pointer to a value declared/existing outside the scope of the function and can modify it.
This is a good thing. For slices, maps, and other large objects you don't have to copy so much data. And if you want to change the external value of a parameter you need to be explicit about it. It helps folks feel safer when calling functions.
Golang is also strongly typed. Every variable has a type, and there is no default conversion. You can't assign float64 to an int and expect it to compile. You need to make an explicit conversion. Again, this is a good thing. You might need to type a little more code, but you're never going to be surprised by an unintended truncation of float to int.
Additionally, as strongly typed as it is, it also has structural typing (almost the same as duck typing, but happens at compile time). It's not
inheritance, but it lets you specify the interface you need a function parameter to have and then you can pass in
any struct
that has supplies that interface. It's a big part of idiomatic Golang and lets you do all
sorts of cool composition.
However, there is a bit of a loophole in there. Because of the way that Method Sets are defined, a pointer to T (*T) implements all of the methods that T does. So according to structural typing T and *T implement the same method set. So far, so good.
But it bit me the other day. Say you have a function that is going to unmarshal a json blob to a struct. You have to tell it what the struct is, so you pass in a pointer to the struct and then use reflection to do the unmarshalling. Since you don't want to have a new function for every type you use a bare `interface{}` as the parameter type. Everything works fine.
func MergeEntity(entityStructPtr interface{}, patch interface{}) interface{} {
...
if err := json.Unmarshal(entityBytes, entityStructPtr); err != nil {
panic(err)
}
...
}
Unless you call the function without a pointer. The compiler can't tell the difference between T and *T in this case because they both implement the bare `interface{}` method set so both
MergeEntity(entity1, entity2)
and
MergeEntity(&entity1, entity2)
compile, but the first one throws a panic from json.Unmarshal
because
entityStructPtr
isn't really a pointer.
And our unit testing didn't cover the combination of options that led to that codepath. So we got a runtime
crash. And worse, from the message/trace, it wasn't obvious what the problem was. So, I fixed it by adding the
missing &
AND I added a bit of guard code to
panic
with a better error message.
paramType := reflect.ValueOf(entityStructPtr)
if paramType.Kind() != reflect.Ptr {
panic("lib.MergeEntity: entityStructPtr must be a pointer not a value")
}
So the next person who runs into the problem will at least know what the problem is without having to do deep introspection of the code.