everything about Golang with common usage patterns.

What is a variadic func?

A variadic func accepts variable number of input values — zero or more. Ellipsis (three-dots) prefix in front of an input type makes a func variadic.

Declares a variadic func with a variadic input param of strings named as `names`.

✪ A simple variadic func

This func returns the passed params as a string separated with spaces.

func toFullname(names ...string) string {
return strings.Join(names, " ")
}

✪ You can pass zero or more params

toFullname("carl", "sagan")
// output: "carl sagan"
toFullname("carl")
// output: "carl"
toFullname()
// output: ""

When to use a variadic func?

  • To skip creating a temporary slice just to pass to a func
  • When the number of input params are unknown
  • To express your intent to increase the readability

Example:

Look at Go Stdlib’s fmt.Println func to understand how it makes itself easy to use.

It uses a variadic func to accept an optional number of input params.

func Println(a ...interface{})

If it wasn’t a variadic func, it’d look like this:

func Println(params []interface{})

You would need to pass a slice to use it — verbose, yes!:

fmt.Println([]interface{}{"hello", "world"})

In its original variadic form, it’s pleasant to use:

fmt.Println("hello", "world")
fmt.Println("hello")
fmt.Println()

After this part, there will be examples about the details of Variadic Funcs and the common usage patterns.

✪ Slices and the Variadic Funcs

A variadic param gets converted to a “new” slice inside the func. A variadic param is actually a syntactic sugar for an input parameter of a slice type.

Using without params

A variadic param becomes a nil slice inside the func when you don’t pass any values to it.

All non-empty slices have associated underlying arrays. A nil slice has no underlying array, yet.

func toFullname(names ...string) []string {
return names
}
// names's underlying array: nil

However, when you add items to it, it’ll have an associated underlying array with the items you appended. It’ll no longer be a nil slice.

Go “append” built-in func appends items to an existing slice and returns the same slice back. Append itself is a variadic func.

func toFullname(names ...string) []string {
return append(names, "hey", "what's up?")
}
toFullname()
// output: [hey what's up?]

How to pass an existing slice?

You can pass an existing slice to a variadic func by post-fixing it with the variadic param operator: “…”.

names := []string{"carl", "sagan")}
toFullname(names...)
// output: "carl sagan"

This is like passing the params as usual:

toFullname("carl", "sagan")

However, with one difference: The passed slice will be used inside the func; no new slice will be created. More on this in the following section.

You can also pass arrays to a variadic func by converting them to slices like this:

names := [2]string{"carl", "sagan"}
toFullname(names[:]...)

Passed slice’s spooky action at a distance

Suppose that you pass an existing slice to a variadic func:

dennis := []string{"dennis", "ritchie"}
toFullname(dennis...)

Suppose also that you change the first item of the variadic param inside the func:

func toFullname(names ...string) string {
names[0] = "guy"
return strings.Join(names, " ")
}

Changing it will also affect the original slice. “dennis” slice now becomes:

[]string{"guy", "ritchie"}

Instead of the original value of:

[]string{"dennis", "ritchie"}

Because the passed slice shares the same underlying array with the slice inside the func, changing its value inside the func also effects the passed slice:

If you have passed params directly (without a slice) then this won’t have happened.

Passing multiple slices on-the-fly

Suppose that we want add “mr.” in front of the slice before passing it to the func.

names := []string{"carl", "sagan"}

This will append the slice to another slice by expanding it first for the append variadic func and then expanding the resulting slice again for the toFullname variadic func:

toFullname(append([]string{"mr."}, names...)...)
// output: "mr. carl sagan"

This is the same as this code:

names = append([]string{"mr."}, "carl", "sagan")
toFullname(names...)
// or with this:
toFullname([]string{"mr.", "carl", "sagan"}...)
// or with this—except passing an existing slice:
toFullname("mr.", "carl", "sagan")

Returning the passed slice

You can’t use a variadic param as a result type, but, you can return it as a slice.

func f(nums ...int) []int {
nums[1] = 10
return nums
}

When you pass a slice to f, it will return an identical new slice. The passed and the returned slices will be connected. Any change to one of them will effect the others.

nums  := []int{23, 45, 67}
nums2 := f(nums...)

Here, nums and nums2 have the same elements. Because, they all point to the same underlying array.

nums  = []int{10, 45, 67}
nums2 = []int{10, 45, 67}
👉 Contains detailed explanations about slice’s underlying array

Expanding operator anti-pattern

If you have funcs which their only purpose is to accept variable number of arguments, then don’t use a slice, use variadic funcs instead.

// Don't do this
toFullname([]string{"rob", "pike"}...)
// Do this
toFullname("rob", "pike")

Using the length of a variadic param

You can use the length of a variadic param to change the behavior of your funcs.

func ToIP(parts ...byte) string {
parts = append(parts, make([]byte, 4-len(parts))...)
  return fmt.Sprintf("%d.%d.%d.%d", 
parts[0], parts[1], parts[2], parts[3])
}

ToIP func takes “parts” as a variadic param and uses parts param’s length to return an IP address as a string with default values — 0.

ToIP(255)   // 255.0.0.0
ToIP(10, 1) // 10.1.0.0
ToIP(127, 0, 0, 1) // 127.0.0.1

✪ Signature of a variadic func

Even though a variadic func is a syntactic sugar; its signature — type identity — is different than a func which accepts a slice.

So, for example, what’s the difference between []string and …string?

A variadic func’s signature:

func PrintVariadic(msgs ...string)
// signature: func(msgs ...string)

A non-variadic func’s signature:

func PrintSlice(msgs []string)
// signature: func([]string)

Their type-identities are not the same. Let’s assign them to variables:

variadic := PrintVariadic
// variadic is a func(...string)
slicey := PrintSlice
// slice is a func([]string)

So, one of them can’t be used in the place of the other interchangeably:

slicey = variadic
// error: type mismatch

✪ Mixing variadic and non-variadics params

You can mix the non-variadic input params with a variadic param by putting the non-variadic params before the variadic param.

func toFullname(id int, names ...string) string {
return fmt.Sprintf("#%02d: %s", id, strings.Join(names, " "))
}
toFullname(1, "carl", "sagan")
// output: "#01: carl sagan"

However, you can’t declare more params after a variadic param:

func toFullname(id int, names ...string, age int) string {}
// error

Accepting variable types of arguments

For example, Go Stdlib’s Printf variadic func accepts any type of input params by using an empty interface type. You can use the empty interface to accept an unknown type and number of arguments in your own code as well.

func Printf(format string, a ...interface{}) (n int, err error) {
  /* this is a pass-through with a... */
  return Fprintf(os.Stdout, format, a...)
}
fmt.Printf("%d %s %f", 1, "string", 3.14)
// output: "1 string 3.14"

Why does not Printf only accept just one variadic param?

When you look at the signature of Printf, it takes a string named as format and a variadic param.

func Printf(format string, a ...interface{})

This is because the format is a required param. Printf forces you to provide it or it will fail to compile.

If it was accepting all of its params through one variadic param, then the caller may not have supplied the necessary formatter param or it wouldn’t be as explicit as this one from the readability perpective. It clearly marks what Printf needs.

Also, when it’s not called with its variadic param: “a”, it will prevent Printf to create an unnecessary slice inside the func — passes a nil slice as we saw earlier. This may not be a clear win for Printf but it can be for you in your own code.

You can use the same pattern in your own code as well.

Beware the empty interface type

interface{} type is also called the empty interface type which means that it bypasses the Go’s static type checking semantics but itself. Using it unnecessarily will cause you more harm than good.

For example, it may force you to use reflection which is a run-time feature (instead of fast and safe — compile-time). You may need to find the type errors by yourself instead of depending on the compiler to find them for you.

Think carefully before using the empty interface. Lean on the explicit types and interfaces to implement the behavior you need.

Passing a slice to variadic param with an empty-interface

You can’t pass an ordinary slice to a variadic param with a type of empty-interface. Why? Read here.

hellos := []string{"hi", "hello", "merhaba"}

You expect this to work, but it doesn’t:

fmt.Println(hellos...)

Because, hellos is a string slice, not an empty-interface slice. A variadic param or a slice can only belong to one type.

You need to convert hellos slice into an empty-interface slice first:

var ihellos []interface{} = make([]interface{}, len(hello))
for i, hello := range hellos {
ihellos[i] = hello
}

Now, the expansion operator will work:

fmt.Println(ihellos...)
// output: [hi hello merhaba]

✪ Functional aspects

You can also use a variadic func that accepts a variable number of funcs. Let’s declare a new formatter func type. A formatter func takes and returns a string:

type formatter func(s string) string

Let’s declare a variadic func which takes a string and an optional number of formatter types to format a string through some stages of pipelines.

func format(s string, fmtrs ...formatter) string {
for _, fmtr := range fmtrs {
s = fmtr(s)
}
  return s
}
format(" alan turing ", trim, last, strings.ToUpper)
// output: TURING
With explanations on how the above code works

You can also use channels, structs, etc. instead of funcs for this chaining pattern. See this or this for example.

Using a func’s result slice as a variadic param

Let’s reuse the “format func” above to create a reusable formatting pipeline builder:

func build(f string) []formatter {
switch f {
case "lastUpper":
return []formatter{trim, last, strings.ToUpper}
case "trimUpper":
return []formatter{trim, strings.ToUpper}
// ...etc
default:
return identityFormatter
}
}

Then run it with the expand operator to provide its result to the format func:

format(" alan turing ", build("lastUpper")...)
// output: TURING
See the details about how the above snippet works

Variadic options pattern

You may have already be familiar with this pattern from other OOP langs and this has been re-popularized again in Go by Rob Pike here back in 2014. It’s like the visitor pattern.

This example maybe advanced for you. Please ask me about the parts that you didn’t understand.

Let’s create a Logger which the verbosity and the prefix options can be changed at the run-time using the option pattern:

type Logger struct {
verbosity
prefix string
}

SetOptions applies options to the Logger to change its behavior using a variadic option param:

func (lo *Logger) SetOptions(opts ...option) {
for _, applyOptTo := range opts {
applyOptTo(lo)
}
}

Let’s create some funcs which return option func as a result in a closure to change the Logger’s behavior:

func HighVerbosity() option {
return func(lo *Logger) {
lo.verbosity = High
}
}
func Prefix(s string) option {
return func(lo *Logger) {
lo.prefix = s
}
}

Now, let’s create a new Logger with the default options:

logger := &Logger{}

Then provide options to the logger through the variadic param:

logger.SetOptions(
HighVerbosity(),
Prefix("ZOMBIE CONTROL"),
)

Now let’s check the output:

logger.Critical("zombie outbreak!")
// [ZOMBIE CONTROL] CRITICAL: zombie outbreak!
logger.Info("1 second passed")
// [ZOMBIE CONTROL] INFO: 1 second passed
See the working code with the explanations inside

✪ More and more brain-food

  • In Go 2, there are some plans to change the behavior of the variadic funcs, read here, here, and here.
  • You can find formal explanations about variadic funcs in Go Spec, here, here, here and here.
  • Using variadic funcs from C.
  • You can find a lot of languages’ varidadic function declarations here. Feel free to explore!

See you until the next tutorial!

💓 Share this with your friends. Thank you!



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here