120

★ Ultimate Guide to Go Variadic Funcs – Learn Go Programming

 6 years ago
source link: https://blog.learngoprogramming.com/golang-variadic-funcs-how-to-patterns-369408f19085?gi=723421efffec
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

★ Ultimate Guide to Go Variadic Functions

Learn everything about Golang variadic funcs with common usage patterns.

1*Su0hDDMVLwDsNPeiJTHEHw.png

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.

1*VmY8owzjVTYfyfMgbIJ1eQ.png

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

1*a8m_shZN4211OlW4_QzjXw.png

✪ 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, " ")
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

✪ You can pass zero or more params

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

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
1*_rdOI_SbL9m07QBcKMqz5Q.png

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()
1*eb78eTgBSqKTasWiFe3y7A.png

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

1*eb78eTgBSqKTasWiFe3y7A.png

✪ Slices and the Variadic Funcs

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

1*U0zC8HO1g-L9ElrX8t_Kkg.png

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

Using without params

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

1*B9__qDxlYLG7wsJDhXgvZA.png
1*_rdOI_SbL9m07QBcKMqz5Q.png

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
1*_rdOI_SbL9m07QBcKMqz5Q.png

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?]

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

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")
1*_rdOI_SbL9m07QBcKMqz5Q.png

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.

1*wc_Yjqa7QHjgfi5N2HUOMg.png
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

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

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...)
1*_rdOI_SbL9m07QBcKMqz5Q.png

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, " ")
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

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

Instead of the original value of:

[]string{"dennis", "ritchie"}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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:

1*lgHDa4zJ012wlOUDIqNwmA.png
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

Passing multiple slices on-the-fly

Suppose that we want to 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")

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

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
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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 affect the others.

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

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}

1*FFqiVq-lvmbRxzT6gOCGlQ.png

👉 Contains detailed explanations about slice’s underlying array

1*a8m_shZN4211OlW4_QzjXw.png

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")

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

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])
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*eb78eTgBSqKTasWiFe3y7A.png

✪ 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?

1*_rdOI_SbL9m07QBcKMqz5Q.png

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)
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*eb78eTgBSqKTasWiFe3y7A.png

✪ 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"
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*a8m_shZN4211OlW4_QzjXw.png

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"
1*_rdOI_SbL9m07QBcKMqz5Q.png

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 perspective. 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.

1*_rdOI_SbL9m07QBcKMqz5Q.png

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.

1*a8m_shZN4211OlW4_QzjXw.png

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...)
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*_rdOI_SbL9m07QBcKMqz5Q.png

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]

1*FFqiVq-lvmbRxzT6gOCGlQ.png
1*eb78eTgBSqKTasWiFe3y7A.png

✪ Functional programming 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
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png

With explanations on how the above code works

1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*a8m_shZN4211OlW4_QzjXw.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png

See the details about how the above snippet works

1*a8m_shZN4211OlW4_QzjXw.png

Variadic options pattern

You may have already been 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.

1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*_rdOI_SbL9m07QBcKMqz5Q.png

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
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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)
}
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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
}
}
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

logger := &Logger{}
1*_rdOI_SbL9m07QBcKMqz5Q.png

Then provide options to the logger through the variadic param:

logger.SetOptions(
HighVerbosity(),
Prefix("ZOMBIE CONTROL"),
)
1*_rdOI_SbL9m07QBcKMqz5Q.png

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

1*FFqiVq-lvmbRxzT6gOCGlQ.png

See the working code with the explanations inside

1*eb78eTgBSqKTasWiFe3y7A.png

✪ 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’ variadic function declarations here. Feel free to explore!
1*-OQBs5b4u65NRQM8aFukAw.png

Alright, that’s all for now. Thank you for reading so far.

Let’s stay in touch:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK