← Back to DevBytes

Go Type System: Static vs Dynamic Typing

Understanding Go’s Type System

Go is a statically typed language. That means every variable, function parameter, and return value has a defined type at compile time. The compiler checks type compatibility before the program ever runs. This stands in contrast to dynamically typed languages like Python or JavaScript, where types are resolved at runtime and variables can freely change their type.

However, Go also provides mechanisms—most notably interfaces—that allow you to write code that works with values of any type, giving a taste of dynamic dispatch while preserving compile‑time safety. This article explores how static typing works in Go, why it matters, and how to use interfaces to achieve the flexibility you might expect from dynamic languages.

Static Typing: The Foundation of Go

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Declaring Variables with Explicit Types

In Go, you can declare a variable and explicitly specify its type. The compiler will then enforce that only values of that type can be assigned to it. This is the most straightforward form of static typing.

package main

import "fmt"

func main() {
    var message string        // declared as string
    message = "Hello, Go"
    // message = 42            // compile error: cannot use 42 as string
    fmt.Println(message)

    var count int = 10        // explicit type with initialization
    fmt.Println(count)
}

Type Inference with Short Declarations

Go supports type inference through the := short variable declaration. The compiler infers the type from the right‑hand side expression, but the variable remains statically typed—its type is fixed at the point of declaration and cannot change later.

package main

import "fmt"

func main() {
    name := "Gopher"          // inferred as string
    // name = 3.14            // compile error: cannot use 3.14 as string

    age := 25                 // inferred as int
    fmt.Println(name, age)

    // Function return type also inferred by compiler
    sum := add(5, 3)          // inferred as int because add returns int
    fmt.Println(sum)
}

func add(a, b int) int {
    return a + b
}

Strict Type Conversions

Go never performs implicit type conversions. You must explicitly convert a value to the desired type when it is compatible. This avoids accidental loss of data and makes type handling transparent.

package main

import "fmt"

func main() {
    var integer int = 42
    var floating float64 = float64(integer)   // explicit conversion required
    // var wrong float64 = integer            // compile error
    fmt.Println(floating)

    var a int32 = 100
    var b int64 = int64(a)                    // even between similar types
    fmt.Println(b)
}

Dynamic Behavior Through Interfaces

While Go’s core is rigidly static, interfaces introduce a powerful form of polymorphism. An interface type defines a set of method signatures. Any concrete type that implements those methods automatically satisfies the interface—no explicit declaration is needed. This lets you write functions that accept any value that “acts like” something, without knowing its exact underlying type.

The Empty Interface: interface{}

The empty interface interface{} has zero methods, which means every type in Go satisfies it. It works like a universal container: you can pass any value into a function expecting interface{}. But to use the concrete value, you need to extract it using a type assertion or a type switch.

package main

import "fmt"

func describe(i interface{}) {
    fmt.Printf("Value: %v, Type: %T\n", i, i)
}

func main() {
    describe(42)                // Value: 42, Type: int
    describe("gopher")          // Value: gopher, Type: string
    describe(3.1415)            // Value: 3.1415, Type: float64
    describe([]int{1, 2, 3})    // Value: [1 2 3], Type: []int
}

Type Assertions: Unpacking the Concrete Type

A type assertion extracts the underlying concrete value from an interface. It can be written in two forms: the single‑return form (which panics if the type doesn’t match) and the safe, two‑value comma‑ok form.

package main

import "fmt"

func main() {
    var data interface{} = "hello"

    // Unsafe: panics if data is not a string
    str := data.(string)
    fmt.Println(str) // "hello"

    // Safe: comma-ok pattern
    value, ok := data.(string)
    if ok {
        fmt.Println("String value:", value)
    } else {
        fmt.Println("Not a string")
    }

    // Attempting wrong type assertion
    num, ok := data.(int)
    if !ok {
        fmt.Println("data is not an int")  // prints this
    }
    _ = num
}

Type Switches: Handling Multiple Dynamic Types

A type switch allows you to test an interface value against several concrete types in a clean, readable way. It’s the idiomatic Go approach for dispatching logic based on the dynamic type.

package main

import "fmt"

func inspect(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String of length %d: %q\n", len(v), v)
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    default:
        fmt.Printf("Unknown type %T\n", v)
    }
}

func main() {
    inspect(42)
    inspect("gopher")
    inspect(true)
    inspect(3.14)
}

Why Static Typing Matters in Go

Static typing is not just a language design choice—it brings concrete engineering benefits:

Best Practices for Working with Go’s Type System

To get the most out of Go’s hybrid approach—static types with flexible interfaces—keep these practices in mind:

Generics Example: A Type‑Safe Generic Function

package main

import "fmt"

// Min returns the smaller of two values of the same ordered type.
func Min[T int | float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min[int](3, 5))          // 3
    fmt.Println(Min(2.71, 3.14))         // 2.71 (type inference works)
    // fmt.Println(Min("a", "b"))         // compile error: string not in constraint
}

This function is statically typed at compile time for whatever type you instantiate it with, yet it avoids the boilerplate of writing separate functions for each numeric type.

Conclusion

Go’s type system is fundamentally static: every value has a concrete type known at compile time. This gives you speed, reliability, and excellent tooling. At the same time, interfaces—especially the empty interface and type switches—let you safely escape rigid typing when you need to handle values of unknown or varying types. By combining concrete types for most of your code with targeted use of interfaces and generics, you can write Go programs that are both safe and expressive. The key is to lean on the compiler as your first line of defense, and use dynamic dispatch only where it truly adds value.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles