In Go, we can define functions that take a variable number of arguments, by defining a "rest" argument. When calling such a function, one can pass the elements of a slice as the "variadic" arguments with the ...
syntax, as in:
elems := []string{"usr", "local", "bin"}
p := filepath.Join(elems...)
However, this only works if the slice:
- lines up exactly with the "rest" argument
- has exactly the same element type as that argument
I have seen both of these snags trip me up, as well as others. For example, regarding 1., I once tried to build a path with the following code:
return filepath.Join(h, ".mflg", elems...)
Due to the above limitation, I had to do this contraption instead, which is much less clear:
return filepath.Join(append([]string{h, ".mflg"}, elems...)...), nil
The second limitation, on the other hand, tends to come up when expanding a slice to pass to Printf
. As an example, while doing one of the exercises in the Go tour, a colleague tried to print an IP address like this:
func (ip IPAddr) String() string {
return fmt.Sprintf("%d.%d.%d.%d", ip[:]...)
}
This did not work, requiring them to expand the arguments by hand:
return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
This is, of course, because Sprintf
's "rest" argument is effectively of type []interface{}
and ip[:]
of type []byte
. Explaining this to my colleague turned out to be somewhat difficult; I had to talk about interface conversions and show them some examples of equivalent functions and calls with regular slice arguments to get them to understand why Go works this way. I don't think this (ostensibly) simple feature should be so complicated to explain.
In both cases, Go's current behaviour is logical, but unintuitive, especially when compared to other languages with similar features, like Python or Ruby. This can trip up both newcomers as well as more experienced users such as myself. In addition, the workarounds for both limitations make the code less concise and harder to read.