Always use typed arguments for functions in Go

Always use typed arguments for functions in Go

Consider you have a function, that accepts two string arguments such as apiKey and apiSecret. Here is a pseudo function to address the problem:

func fooClient(apiKey, apiSecret string) error {
    // do the implementation here...
    panic("not implemented")
}

This function accepts two strings that's it! How can you make sure that the caller is sending arguments in the correct order? It is possible to call this function via;

// "a" or "b", both string values. can be interchangeable. 
if err := fooClient("a","b"); err != nil {
    return fmt.Errorf("fooClient err: %w", err)
}

or, you can call the function via;

// switch args, still callable.
if err := fooClient("b","a"); err != nil {
    return fmt.Errorf("fooClient err: %w", err)
}

Sometimes, we get lazy and skip type declarations to quickly write the code. This situation can lead to making mistakes. In fact, there can be such errors that you might not even detect them because you think you provided the correct arguments. This is called Silent Error or Silent Fail.

What if you define types for arguments?

// FooClientApiKey is a custom type for ApiKey representation.
type FooClientApiKey struct {
    Value string
}

func (f FooClientApiKey) String() string {
    return f.Value
}

// FooClientApiSecret is a custom type for ApiSecret representation.
type FooClientApiSecret struct {
    Value string
}

func (f FooClientApiSecret) String() string {
    return f.Value
}

// fooClient is a poc function to describe safe type usage.
func fooClient(apiKey FooClientApiKey, apiSecret FooClientApiSecret) error {
    fmt.Println("apiKey", apiKey) // uses stringer
    fmt.Println("apiKey", apiKey.Value)

    fmt.Println("apiSecret", apiSecret) // uses stringer
    fmt.Println("apiSecret", apiSecret.Value)
    return nil
}

How do you call this function?

// you can't call it!
if err := fooClient("aaa", "bbb"); err != nil {
    panic(err)
}

// cannot use "aaa" (untyped string constant) as FooClientApiKey value in argument to fooClient
// cannot use "bbb" (untyped string constant) as FooClientSecret value in argument to fooClient

You can call this function via;

// this way, preferred.
if err := fooClient(FooClientApiKey{Value: "apiKey"}, FooClientApiSecret{Value: "apiSecret"}); err != nil {
    panic(err)
}

// or this way, not preferred.
if err := fooClient(FooClientApiKey{"apiKey"}, FooClientApiSecret{"apiSecret"}); err != nil {
    panic(err)
}

untyped string constant usage can lead to issues. If you define the types such as:

type (
    FooClientApiKey string
    FooClientSecret string
)

You'll still face the same issue. Both FooClientApiKey and FooClientSecret is still aliased to string and variables can be used interchangeably. This will still work in the wrong matter:

// both string! and it's callable, leads to silent fail :(
if err := fooClient("aaa", "bbb"); err != nil {
    panic(err)
}

At the end of the day, you'll probably fetch the values from the environment (or kind of secret manager) as a string. To avoid falling into this pitfall, always define your custom types to avoid silent fail!

Complete example:

package main

import "fmt"

// FooClientApiKey is a custom type for ApiKey representation.
type FooClientApiKey struct {
    Value string
}

func (f FooClientApiKey) String() string {
    return f.Value
}

// FooClientApiSecret is a custom type for ApiSecret representation.
type FooClientApiSecret struct {
    Value string
}

func (f FooClientApiSecret) String() string {
    return f.Value
}

// fooClient is a poc function to describe safe/strong type usage.
func fooClient(apiKey FooClientApiKey, apiSecret FooClientApiSecret) error {
    fmt.Println("apiKey", apiKey)
    fmt.Println("apiKey", apiKey.Value)

    fmt.Println("apiSecret", apiSecret)
    fmt.Println("apiSecret", apiSecret.Value)
    return nil
}

func main() {
    // this way is preferred.
    if err := fooClient(FooClientApiKey{Value: "apiKey"}, FooClientApiSecret{Value: "apiSecret"}); err != nil {
        panic(err)
    }

    // or this way, not preferred :).
    if err := fooClient(FooClientApiKey{"apiKey"}, FooClientApiSecret{"apiSecret"}); err != nil {
        panic(err)
    }
}

Happy programming!