Home > Net >  In Go, can you have function parameters with two distinct types?
In Go, can you have function parameters with two distinct types?

Time:01-03

Can you write a function in Go whose parameter can be two different types? For example, if I write a simple function that takes in an array/slice of int and simply returns the first value:

func First(array []int) int {
  return array[0]
}

Is there a way to type this such that we can also pass in a []string, etc.? In TypeScript for example, we can do it like such without having to type the array as any:

const first = (array: (number | string)[]): number | string => {
  return array[0];
};

I've seen answers explaining the use of interface{} for situations like this... and maybe that's the only way, but it seems to close to any in TS and it feels like there might be a better way of doing this.

CodePudding user response:

(I haven't used Go for a few years, long before they introduced generics - so I'm basing my answer off their documentation for generics)

TypeScript's "generics" (in quotes) aren't really comparable to Go's generics. TypeScript is all about being able to describe an interface (in an abstract sense) for a runtime system built around type-erasure (i.e. JavaScript), while Go's is... honestly, I have no idea. I just don't know how Go's generics are implemented nor their runtime characteristics: Articles on Go's generics, even from the language authors blog or their own documentation site fail to mention key-terms like type erasure, reified generics, (template) instantiation or monomorph, so if anyone has a better understanding please edit this post, or let me know in a comment!

Anyway, the good news is that as-of the Go 1.18 Beta, its support for generics includes support for generic constraints, but also support for union types as a constraint for generic type parameters (though I haven't yet found any information regarding support for other ADTs like product types and intersection types).

(Note that, at least for now, Go won't support union types as concrete types, but in practice that shouldn't be an issue)


In your case, if you want a function that returns the first element of a slice that could be either []int or []string (or returns some default-value if the slice is empty), then you can do this:

func First[T int | string](arr []T, ifEmpty T) T {
    for _, v := range arr {
        return v
    }

    return ifEmpty
}

While at first-glance you might think that this would allow for the arr slice to simultaneously contain both int and string values, this is not allowed (see below). Remember that generic parameters arguments are supplied by the caller and have to be valid concrete types, so First can only be instantiated as either First[int] or First[string], which in-turn implies that arr can be either []int or []string.

Anyway, the full example below compiles and runs in the Go dev branch on the Go playground:

package main

import "fmt"

func First[T int | string](arr []T, ifEmpty T) T {
    for _, v := range arr {
        return v
    }

    return ifEmpty
}

func main() {

    // First[int]:
    arrayOfInts := []int{2, 3, 5, 7, 11, 13}

    firstInt := First(arrayOfInts, -1)
    fmt.Println(firstInt) // 2

    // First[string]:
    arrayOfStrings := []string{"life", "is short", "and love is always over", "in the morning"}
    
    firstString := First(arrayOfStrings, "empty")
    fmt.Println(firstString) // "life"

}

You can also extract the constraint T int | string and move it to an interface, and then use that as the constraint, which I personally think is easier to read, especially when you might need to repeat the same constraint in multiple places:

type IntOrString interface {
    int | string
}

func First[T IntOrString](arr []T, ifEmpty T) T {
    for _, v := range arr {
        return v
    }

    return ifEmpty
}

Things you can't do...

Note that Go does not (currently, at least) allow using a type that describes a union as a variable's type by itself (nor can you use as a slice's element type either); you can only use a union as a constraint, otherwise you'll get the "interface contains type constraints" error. Which means you can't describe an array that can contain both int and string values and then use that interface for a concrete array type:

package main

import "fmt"

type IntOrString interface {
    int | string
}

func First[T IntOrString](arr []T, ifEmpty T) T {
    for _, v := range arr {
        return v
    }

    return ifEmpty
}

func main() {

    arrayOfIntsOrStrings := []IntOrString{2, "foo", 3, "bar", 5, "baz", 7, 11, 13} // ERROR: interface contains type constraints

    firstValue := First(arrayOfIntsOrStrings, -1)
    fmt.Println(firstValue)
}

./prog.go:19:28: interface contains type constraints
Go build failed.


CodePudding user response:

kiss

package main

import (
    "fmt"
    "reflect"
)

// go with generics support
func First[T any](s []T) (zeroValue T, ok bool) {
    if len(s) > 0 {
        return s[0], true
    }
    return
}

// go without generics support
func First1(s interface{}) (interface{}, bool) {
    switch x := s.(type) {
    case []string:
        if len(x) > 0 {
            return x[0], true
        }
    case []int:
        if len(x) > 0 {
            return x[0], true
        }
    }

    rt := reflect.TypeOf(s)
    if rt.Kind() == reflect.Slice {
        rv := reflect.ValueOf(s)
        if rv.Len() > 0 {
            return rv.Index(0).Interface(), true
        }
    }
    return nil, false
}

func main() {

    xs := []int{2, 3, 5, 7, 11, 13}
    fmt.Println(First(xs))
    fmt.Println(First1(xs))

    ys := []string{"life", "is short", "and love is always over", "in the morning"}
    fmt.Println(First(ys))
    fmt.Println(First1(ys))

}

https://go.dev/play/p/VF14tUj4moU?v=gotip

  •  Tags:  
  • go
  • Related