Skip to content

Go Function Return Values: List, Not Tuple

Recently, I suddenly found that the following code line is weird, how could it pass (int, error) to ...any?

func main
func variadic(a ...any) {
    fmt.Println(a)
    return
}

func array(a []any) {
    fmt.Println(a...)
    return
}

func main() {
    variadic(json.Marshal(""))
    // array(json.Marshal(""))
    // ^ CANNOT PASS COMPILE
}

I checked the Go Spec, and the only related topic is tuple:

As a special case, if the return values of a function or method g are equal in number and individually assignable to the parameters of another function or method f, then the call f(g(parameters_of_g)) will invoke f after binding the return values of g to the parameters of f in order. The call of f must contain no parameters other than the call of g, and g must have at least one return value. If f has a final ... parameter, it is assigned the return values of g that remain after assignment of regular parameters.

However, it doesn't explain why the ([]byte, error) could be parsed to ...any. Hence, I try to find an answer in the source code.

Return Values: List, not Tuple

Non First-Class Tuple

Go language definitely doesn't support tuples, at least not a first-class tuple. The go/types/tuple.go reveals that tuple represents an ordered list of variables.

// A Tuple represents an ordered list of variables; a nil *Tuple is a valid (empty) tuple.
// Tuples are used as components of signatures and to represent the type of multiple
// assignments; they are not first class types of Go.
type Tuple struct {
    vars []*Var
}

// A Variable represents a declared variable (including function parameters and results, and struct fields).
type Var struct {
    object
    embedded bool // if set, the variable is an embedded struct field, and name is the type name
    isField  bool // var is struct field
    used     bool // set if the variable was used
    origin   *Var // if non-nil, the Var from which this one was instantiated
}

According to the implementation, we can find that the returned values(results) are represented by a tuple, which is vital to the topic here. We can claim that inside Go, there is no formal tuple definition in the other languages.

Results Are Many, Not Single.

The question above is indeed an assignment problem, which wonders about the (int, error) cannot be assigned to any, but can be assigned to ...any.

After checking the definition of the tuple, the anwser is natural.

Because the returned values(results) are a list of variables represented by Go internal strucutre Tuple , not a single variable holds all values, they could be assigned to ...any.

The Flow of Assignment

After learning about the representation of function results/parameters, I provided some invalid assignments and verified them with the Go source code.

Invalid Assignment

func main() {
    var i, i2, a int
    i, i2 = return2()

    tuple := return2()
    // ^ invalid assignment, assignment mismatch:
    // 1 variable but return2 returns 2 values

    i, i2, a = return2(), 1
    // ^ invalid assignment, assignment mismatch:
    // 3 variables but 2 values

    a, i, i2 = 1, return2()
    // ^ invalid assignment, assignment mismatch:
    // 3 variables but 2 values
}

The invalid assignments are constrained by Go itself, in go/types/assignments.go#assignVars.

It contains 2 cases, a n:n mapping from left to right and not. The n:n mapping refers to the exact count of values between two sides of =.

    var a, b int
    a, b = 1, 2
func return2() (int, int) {
    return 1, 2
}

func main() {
    var a, b int
    a, b = return2()
}

The assignVars checks if the left side has the exact count of the right side, if equal, assign them in sequence with type checking. It contains some logic checks as the following flow.

Method assignVars
// assignVars type-checks assignments of expressions orig_rhs to variables lhs.
func (check *Checker) assignVars(lhs, orig_rhs []ast.Expr) {
    l, r := len(lhs), len(orig_rhs)

    // If l == 1 and the rhs is a single call, for a better
    // error message don't handle it as n:n mapping below.
    isCall := false
    if r == 1 {
        _, isCall = unparen(orig_rhs[0]).(*ast.CallExpr)
    }

    // If we have a n:n mapping from lhs variable to rhs expression,
    // each value can be assigned to its corresponding variable.
    if l == r && !isCall {
        for i, lhs := range lhs {
            check.assignVar(lhs, orig_rhs[i], nil)
        }
        return
    }

    // If we don't have an n:n mapping, the rhs must be a single expression
    // resulting in 2 or more values; otherwise we have an assignment mismatch.
    if r != 1 {
        // Only report a mismatch error if there are no other errors on the lhs or rhs.
        okLHS := check.useLHS(lhs...)
        okRHS := check.use(orig_rhs...)
        if okLHS && okRHS {
            check.assignError(orig_rhs, l, r)
        }
        return
    }

    rhs, commaOk := check.multiExpr(orig_rhs[0], l == 2)
    r = len(rhs)
    if l == r {
        for i, lhs := range lhs {
            check.assignVar(lhs, nil, rhs[i])
        }
        // Only record comma-ok expression if both assignments succeeded
        // (go.dev/issue/59371).
        if commaOk && rhs[0].mode != invalid && rhs[1].mode != invalid {
            check.recordCommaOkTypes(orig_rhs[0], rhs)
        }
        return
    }

    // In all other cases we have an assignment mismatch.
    // Only report a mismatch error if there are no other errors on the rhs.
    if rhs[0].mode != invalid {
        check.assignError(orig_rhs, l, r)
    }
    check.useLHS(lhs...)
    // orig_rhs[0] was already evaluated
}

Retrieve Expression Results

During assignments, the method assignVars retrieves the length of returned values of the expression. It retrieves the raw expression and then gets the underlying returned types.

get return values
// multiExpr typechecks e and returns its value (or values) in list.
// If allowCommaOk is set and e is a map index, comma-ok, or comma-err
// expression, the result is a two-element list containing the value
// of e, and an untyped bool value or an error value, respectively.
// If an error occurred, list[0] is not valid.
func (check *Checker) multiExpr(e ast.Expr, allowCommaOk bool) (list []*operand, commaOk bool) {
    var x operand
    check.rawExpr(nil, &x, e, nil, false)
    check.exclude(&x, 1<<novalue|1<<builtin|1<<typexpr)

    if t, ok := x.typ.(*Tuple); ok && x.mode != invalid {
        // multiple values
        list = make([]*operand, t.Len())
        for i, v := range t.vars {
            list[i] = &operand{mode: value, expr: e, typ: v.typ}
        }
        return
    }

    // exactly one (possibly invalid or comma-ok) value
    list = []*operand{&x}
    if allowCommaOk && (x.mode == mapindex || x.mode == commaok || x.mode == commaerr) {
        x2 := &operand{mode: value, expr: e, typ: Typ[UntypedBool]}
        if x.mode == commaerr {
            x2.typ = universeError
        }
        list = append(list, x2)
        commaOk = true
    }

    return
}