DMarlon Studio

Lesson 2 · 2 of 20

Empty interface and type switches · Go

Store values of any type with any (interface{}) in Go and get the concrete type back with type assertions and type switches.

Go · Empty interface and type switches in DMarlon Studio: the example's code and its output in the terminal

The empty interface interface{} asks for no methods, so every type satisfies it: a variable of that type can hold any value. Since Go 1.18 it has a shorter name, `any`. values is a []any with an integer, a decimal, some text, a list, a boolean and nil, much like settings read from a configuration file.

An any keeps the value and its type, but to work with it you need the concrete type back. The type switch switch v := v.(type) does that: inside each case, v already has that type, which is why len(v) works in case string and the items can be counted in case []string. case nil catches the missing value and default everything else; %T prints the type name (bool).

For a single type there is the type assertion. With two results, n, ok := port.(int), nothing can go wrong: port holds the text "8080", not an integer, so ok is false and n keeps the zero value, 0.

With one result, values[0].(int) returns the integer directly (8080 * 2 = 16160), but if the type did not match it would panic and stop the program. Use it only when you are sure of the type; when in doubt, use ok or a switch.

any is handy at the edges of a program, such as JSON with no fixed shape or fmt.Println itself, but it gives up the compiler's type checks. Inside your own code, prefer concrete types, interfaces with methods or generics.

Code · main.go

package mainimport "fmt"// describe uses a type switch: in each case, v already has that concrete type.func describe(v interface{}) string {	switch v := v.(type) {	case nil:		return "nil: the setting is missing"	case int:		return fmt.Sprintf("int %d", v)	case float64:		return fmt.Sprintf("float64 %.2f", v)	case string:		return fmt.Sprintf("string %q (%d bytes)", v, len(v))	case []string:		return fmt.Sprintf("[]string with %d items: %v", len(v), v)	default:		return fmt.Sprintf("something else: %T", v)	}}func main() {	// any is another name for interface{}: it can hold a value of any type.	values := []any{8080, 0.75, "shop.local", []string{"api", "eu"}, true, nil}	for _, v := range values {		fmt.Println(describe(v))	}	var port any = "8080"	n, ok := port.(int) // comma-ok form: never panics	fmt.Println("port as int:", n, ok)	fmt.Println("first value doubled:", values[0].(int)*2) // panics if wrong}

Output · go run main.go

int 8080
float64 0.75
string "shop.local" (10 bytes)
[]string with 2 items: [api eu]
something else: bool
nil: the setting is missing
port as int: 0 false
first value doubled: 16160

Key points

  • any is the same as interface{}: it accepts any value.
  • switch v := x.(type) gives v the type of each case.
  • n, ok := x.(int) never fails: ok tells you whether the type matches.
  • x.(int) without ok panics if the type is not the expected one.
  • Use any at the edges; inside, concrete types.

Exercise

Add a case bool to describe that prints flag on or flag off, and change the last line to values[2].(int) to see the panic a wrong assertion causes.