How to Check the Type of a Variable in Go

Use the built-in `fmt.Sprintf("%T", variable)` function or the `reflect` package to inspect a variable's type at runtime.

The problem with dynamic data

You are parsing a configuration file or handling a webhook from a third-party API. The library hands you a map where every value is an any. You grab a field, print it, and see 42. Is that an integer? A float? A string that happens to look like a number? You try to add it to another number and the compiler rejects the code with invalid operation: operator + not defined on string. You need to know what you are holding before you can process it.

Go is a statically typed language. The compiler knows the type of every variable at compile time. If you declare var x int, the compiler guarantees x is an int. You cannot accidentally assign a string to it. This safety is Go's superpower. It catches mistakes before the program runs.

The escape hatch is any (formerly interface{}). An any value can hold a value of any type. Under the hood, Go stores two pieces of information inside an any: the actual value and a pointer to the type description. Checking the type means inspecting that type description. You do this for debugging, for handling dynamic data, or when writing generic tools that must work with arbitrary types.

Quick inspection with fmt

The fastest way to check a type is to print it. The fmt package supports the %T verb, which outputs the type of the argument. This is ideal for debugging or logging. It returns a string representation of the type, which is human-readable but not useful for program logic.

Here is the simplest way to inspect types during development:

package main

import "fmt"

func main() {
	x := 42
	y := "hello"
	z := any(3.14)

	// %T prints the type of the argument, not the value.
	// This is the standard approach for quick debugging output.
	fmt.Printf("x is %T\n", x)
	fmt.Printf("y is %T\n", y)
	fmt.Printf("z is %T\n", z)
}

The output shows x is int, y is string, and z is float64. Note the any(3.14) call. Without the any conversion, 3.14 is an untyped constant. The compiler infers its type based on context. Wrapping it in any forces the compiler to choose a concrete type, which defaults to float64 for floating-point constants.

Convention aside: Go 1.18 introduced any as an alias for interface{}. The community prefers any in new code. It signals that the type is truly dynamic and improves readability. Use any unless you are maintaining legacy code that requires the older spelling.

fmt.Sprintf("%T", v) is for humans, not machines. Use it when you need to log a type or verify your assumptions during development.

Type assertions for control flow

When you need to branch your logic based on the type, use a type assertion. A type assertion extracts the value from an any and checks if it matches a specific type. The syntax is v.(T), where v is the interface value and T is the target type.

There are two forms. The single-result form panics if the type does not match. The two-result form, known as the comma-ok idiom, returns the value and a boolean indicating success. Always use the comma-ok idiom in production code to avoid panics.

Here is a realistic example processing configuration values:

package main

import "fmt"

// ProcessValue converts a dynamic config value to a log-friendly string.
// It uses type assertions to handle known types explicitly.
func ProcessValue(v any) string {
	// The comma-ok idiom attempts to assert v is an int.
	// If successful, i holds the value and ok is true.
	// If v is not an int, i is 0 and ok is false.
	if i, ok := v.(int); ok {
		return fmt.Sprintf("int: %d", i)
	}

	// Check for string. Reusing the pattern keeps logic flat.
	// Type assertions are fast and compile-time checked for the target type.
	if s, ok := v.(string); ok {
		return fmt.Sprintf("str: %s", s)
	}

	// Fallback for unhandled types.
	// In production code, you might return an error here instead.
	return "unknown"
}

func main() {
	fmt.Println(ProcessValue(42))
	fmt.Println(ProcessValue("text"))
	fmt.Println(ProcessValue(3.14))
}

The compiler rejects the program with invalid type assertion: v.(int) (non-interface type string on left) if you try to use a type assertion on a variable that is not an interface. Type assertions only work on any or other interface types.

Convention aside: The comma-ok idiom is the Go standard for safe type assertions. It prevents panics and makes the control flow explicit. Write if val, ok := v.(Type); ok and handle the failure case. Never drop the ok variable unless you are certain of the type and accept the risk of a panic.

Type assertions are for known types. Use them when you expect a small set of types and want to extract the value safely.

The interface box and the nil trap

Understanding how Go implements interfaces prevents a common bug. An interface value is a pair: (type, value). When you check i == nil, you are checking if both the type and the value are nil.

Consider a nil pointer stored in an interface:

package main

import "fmt"

func main() {
	var p *int = nil
	var i any = p

	// i is not nil. i holds a *int type with a nil value.
	// The pair is (*int, nil), which is not equal to (nil, nil).
	fmt.Println(i == nil) // prints: false

	// Accessing the pointer without checking causes a panic.
	// The type is valid, but the value points to nothing.
	// fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
}

The variable i is not nil because it contains a type: *int. The value inside is nil, but the interface itself is populated. This distinction trips up many developers. If you assign a nil pointer to an any, the interface becomes non-nil.

To fix this, check the type before checking the value, or check the value directly if you know the type. If you need to detect a nil interface, ensure you are not storing typed nils inside it.

A nil pointer inside an interface is not a nil interface. Check the type first, or avoid storing nil pointers in dynamic containers.

Reflection for generic tools

When you do not know the types at compile time, or when you are writing a library that must handle arbitrary types, use the reflect package. Reflection lets you inspect and manipulate values dynamically. It is powerful but comes with a cost: reflection is slower than direct code and allocates memory.

The reflect.TypeOf function returns a reflect.Type describing the dynamic type of a value. The reflect.ValueOf function returns a reflect.Value that holds the actual data. You often need both.

Here is how to inspect type metadata:

package main

import (
	"fmt"
	"reflect"
)

// InspectType prints detailed type metadata using reflection.
// This is useful for generic tools like serializers or debuggers.
func InspectType(v any) {
	// TypeOf returns a reflect.Type describing the dynamic type of v.
	// If v is nil, TypeOf returns nil.
	t := reflect.TypeOf(v)
	if t == nil {
		fmt.Println("nil")
		return
	}

	// Kind returns the underlying kind: int, slice, struct, etc.
	// Name returns the type name, or empty for unnamed types like slices.
	fmt.Printf("Kind: %v, Name: %s\n", t.Kind(), t.Name())
}

func main() {
	InspectType(100)
	InspectType("text")
	InspectType([]int{1, 2})
}

The output shows Kind: int, Name: int, Kind: string, Name: string, and Kind: slice, Name: . The Kind is a limited set of categories defined by the reflect package. The Name is the specific type name. For built-in types like int, the name matches the kind. For custom types, the name is the identifier. For unnamed types like slices or maps, the name is empty.

Reflection allocates memory and bypasses compiler optimizations. In a loop processing millions of items, reflection can turn a 10ms operation into 500ms. Use reflection only when you cannot solve the problem with interfaces and type switches.

Reflection is for unknown types. Use it when you are writing generic libraries, serializers, or tools that must inspect arbitrary data structures.

Kind versus Name

The distinction between Kind and Name matters when working with custom types. Kind tells you the underlying category. Name tells you the specific definition.

Define a custom type:

type MyInt int

func main() {
	var x MyInt = 42
	t := reflect.TypeOf(x)

	// Kind is Int because the underlying storage is an int.
	// Name is MyInt because that is the type identifier.
	fmt.Printf("Kind: %v, Name: %s\n", t.Kind(), t.Name())
}

The output is Kind: int, Name: MyInt. If you are checking if a value is an integer, check the Kind. If you are checking if a value is a specific custom type, check the Name or compare the reflect.Type directly.

Use Kind for broad categories like "is this a slice?" or "is this a pointer?". Use Name or type comparison for specific types like "is this a User struct?".

Pitfalls and panics

Type checking introduces runtime risks if you are not careful. The most common error is a panic from a failed type assertion. If you use the single-result form v.(T) and the type does not match, the program crashes.

The compiler rejects the program with panic: interface conversion: any is string, not int if you assert the wrong type without the comma-ok idiom. Always use the comma-ok idiom or a type switch to handle multiple types safely.

Another pitfall is performance. Reflection is slow. If you find yourself using reflection in a hot path, reconsider the design. Often, an interface with a few methods is faster and cleaner than inspecting types dynamically.

The worst type bug is the one that never logs. If you drop an any value without checking its type, you might silently lose data or cause a panic later. Log unexpected types during development to catch design flaws early.

Decision matrix

Choose the right tool based on your needs. Each approach has a specific role in Go code.

Use fmt.Sprintf("%T", v) when you need a quick type string for debugging or logging.

Use a type assertion with the comma-ok idiom when you expect one or two specific types and want to extract the value safely.

Use a type switch when you have multiple distinct types to handle and want clean, exhaustive branching.

Use reflect.TypeOf when you are writing a generic library, serializer, or tool that must inspect arbitrary types dynamically.

Avoid reflection in hot paths; the overhead is measurable and often unnecessary. Prefer interfaces and type switches for application logic.

Type assertions are for known types. Reflection is for unknown types. Trust the compiler when you can.

Where to go next