Skip to content
JavaGo
Part II · Types & Data

any / interface{}

The empty interface is Go’s Object: type assertions, type switches, and the nil-interface trap that catches every Java developer once.

Section 08 of 31 4 min read

any is an alias for interface{}, the interface with no methods. Every type satisfies it. It is Go’s escape hatch for "I genuinely don’t know the type at compile time", and it should be rare in application code.

any vs Java Object

Java — Object and pattern matching
Object val = 42;       // autoboxed to Integer
val = "now a string";

if (val instanceof String s) {   // Java 16+
    System.out.println(s.toUpperCase());
}

String t = (String) val; // ClassCastException if wrong
Go — any and type assertions
var val any = 42
val = "now a string"

// Comma-ok assertion: never panics
if s, ok := val.(string); ok {
    fmt.Println(strings.ToUpper(s))
}

t := val.(string) // panics if val is not a string

Type switches

Java 21 — pattern matching for switch
static String describe(Object val) {
    return switch (val) {
        case String s -> "string: " + s;
        case Integer i -> "int: " + i;
        case byte[] b -> b.length + " bytes";
        case null -> "null";
        default -> "unknown: " + val.getClass();
    };
}
Go — type switch
func describe(val any) string {
    switch v := val.(type) {
    case string:
        return "string: " + v // v is a string here
    case int:
        return fmt.Sprintf("int: %d", v)
    case []byte:
        return fmt.Sprintf("%d bytes", len(v))
    case nil:
        return "nil"
    default:
        return fmt.Sprintf("unknown: %T", v) // %T prints the type
    }
}

Where any shows up in real code

decoding unknown JSON and metadata bags
// Decoding JSON whose shape you do not know
var doc map[string]any
if err := json.Unmarshal(data, &doc); err != nil {
    return err
}

// JSON numbers decode as float64, never int!
count, ok := doc["count"].(float64)   // ok
// n, ok := doc["count"].(int)        // always false

// Nested objects are map[string]any, arrays are []any
tags, _ := doc["tags"].([]any)

// Standard library APIs that take any:
//   fmt.Println(a ...any)
//   json.Marshal(v any)
//   context.WithValue(ctx, key, val any)

The nil interface trap

An interface value has two parts: a type and a value. It is only nil when both are nil. Put a nil pointer into an interface and the interface is no longer nil, because it now knows the type. This bites hardest with the error interface.

return nil

return a nil *ValidationError

Fig. 8.1An interface value carries a type and a value, and it is nil only when both are. Returning a nil *ValidationError as an error fills in the type, so the result is not nil even though nothing failed.
a nil pointer inside an interface is not a nil interface
type ValidationError struct{ Field string }

func (e *ValidationError) Error() string { return e.Field + " is invalid" }

func validate(name string) error {
    var verr *ValidationError // nil pointer
    if name == "" {
        verr = &ValidationError{Field: "name"}
    }
    return verr // BUG: returns (type=*ValidationError, value=nil)
}

err := validate("ada")
fmt.Println(err == nil) // false! The interface holds a type.

// FIX: return a literal nil on the success path
func validate(name string) error {
    if name == "" {
        return &ValidationError{Field: "name"}
    }
    return nil
}
↑ ↓ to navigateEnter to openEsc to close