What Are Go Keywords and Reserved Words

Go keywords are 25 reserved words like func, var, and if that define syntax and cannot be used as identifiers.

The vocabulary of Go

You are writing a handler and you want to name a variable type. The compiler stops you. You are coming from a language with fifty keywords and you are looking for class or extends. You will not find them. Go keeps the vocabulary small on purpose. The designers wanted a language you can read without memorizing a dictionary.

Go has exactly 25 keywords. That is the entire list. Keywords define the syntax. They structure the code. You cannot use a keyword as an identifier. You cannot name a variable func. You cannot name a package if. The compiler treats keywords as tokens, not names. If you try to use one as a name, the parser rejects the program.

Keywords versus predeclared identifiers

The distinction matters. Keywords are reserved by the syntax. Predeclared identifiers are names built into the language but they behave like regular identifiers in the blank package. You can shadow a predeclared identifier locally. You cannot shadow a keyword.

true, false, and nil are predeclared identifiers. They are not keywords. int, string, bool, byte, rune, and error are also predeclared identifiers. append, make, len, cap, close, delete, panic, recover, print, println, complex, real, and imag are predeclared functions or identifiers.

Shadowing a predeclared identifier is legal but confusing. The compiler allows it. Your teammates will not. If you shadow int, you lose the integer type in that scope. If you shadow nil, you lose the untyped nil constant. The community expects these names to mean what they always mean.

package main

import "fmt"

func main() {
    // 'int' is a predeclared identifier, not a keyword.
    // You can shadow it locally, though you should not.
    int := "I am a string now"
    fmt.Println(int)
}

The code above compiles. It prints I am a string now. It also makes the code harder to read. Avoid shadowing predeclared identifiers. Use them for their standard meaning.

The 25 keywords

The keywords fall into groups. Knowing the groups helps you remember them.

Package structure uses package and import. Declarations use var, const, type, struct, interface, map, and chan. Functions use func. Flow control uses if, else, switch, case, default, fallthrough, for, range, break, continue, goto, and return. Concurrency uses go, defer, and select.

range iterates over arrays, slices, maps, and channels. select multiplexes channel operations. go spawns a goroutine. defer schedules a function call to run when the surrounding function returns.

package main

import "fmt"

// Config holds settings. Field names must be valid identifiers.
type Config struct {
    // 'Name' is public because it starts with a capital letter.
    // Public names are exported to other packages.
    Name string
    // 'port' is private. Only this package can access it.
    // Go has no 'private' keyword. Lowercase means private.
    port int
}

func main() {
    // You cannot use 'type' as a field name. It is a keyword.
    // c := Config{Type: "web"} // Error: expected 'IDENT', found 'type'

    c := Config{Name: "server", port: 8080}
    fmt.Println(c.Name)
}

Public names start with a capital letter. Private names start with a lowercase letter. There are no public or private keywords. The capitalization rule is the convention. The compiler enforces it.

Walkthrough of a keyword error

The compiler reads your code in phases. The lexer turns text into tokens. When it sees for, it emits a FOR token. When it sees x, it emits an IDENT token. The parser consumes tokens and builds a syntax tree.

If the parser expects an identifier and sees a keyword, it fails. The error message tells you exactly what happened.

package main

func main() {
    // 'break' is a keyword. You cannot use it as a variable name.
    var break int
}

The compiler rejects this with expected 'IDENT', found 'break'. The parser was looking for an identifier after var. It found the BREAK token instead. The program does not compile.

This strictness prevents ambiguity. If break could be a variable, the compiler would have to guess whether break in a loop refers to the variable or the statement. Go avoids that guess. Keywords are always syntax.

Realistic concurrency example

Keywords work together to build concurrent programs. go, chan, select, defer, and range appear in almost every real Go service.

package main

import (
    "context"
    "fmt"
    "time"
)

// Worker processes items from a channel.
// The receiver name 'w' is short, matching the type name.
// Convention prefers one or two letters for receivers.
type Worker struct {
    name string
}

func (w *Worker) run(ctx context.Context, jobs <-chan Job) {
    // 'defer' schedules cleanup when the function returns.
    // It runs even if the function panics.
    defer w.close()

    // 'for' loops until the channel closes or context cancels.
    for {
        // 'select' waits on multiple channel operations.
        // It blocks until one case can proceed.
        select {
        case <-ctx.Done():
            // Context cancelled. Exit the loop.
            return
        case job, ok := <-jobs:
            if !ok {
                // Channel closed. No more jobs.
                return
            }
            w.process(job)
        }
    }
}

// Job represents a unit of work.
type Job struct {
    ID int
}

func (w *Worker) process(j Job) {
    fmt.Printf("Worker %s processing job %d\n", w.name, j.ID)
}

func (w *Worker) close() {
    fmt.Printf("Worker %s shutting down\n", w.name)
}

func main() {
    // 'context' is plumbing. Run it through every long-lived call site.
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    jobs := make(chan Job, 10)
    w := &Worker{name: "alpha"}

    // 'go' spawns a goroutine. Goroutines are cheap.
    go w.run(ctx, jobs)

    jobs <- Job{ID: 1}
    close(jobs)

    // Wait for goroutine to finish.
    time.Sleep(100 * time.Millisecond)
}

The receiver name is w, not self or this. Go convention uses short names matching the type. context.Context is always the first parameter. Functions that take a context should respect cancellation. defer runs cleanup on return. select handles multiple channels. go starts concurrency.

Goroutines are cheap. Channels are not magic. Always have a cancellation path. The worst goroutine bug is the one that never logs. If a goroutine waits on a channel that never closes, it leaks. Use context or a done channel to break the wait.

Pitfalls and compiler errors

Loop variable capture is a classic trap. range reuses the same variable for each iteration. If you capture that variable in a closure, you capture the variable, not the value.

package main

import "fmt"

func main() {
    items := []string{"a", "b", "c"}

    // 'range' reuses the same variable 'item' for each iteration.
    // Capturing it in a closure captures the variable, not the value.
    for _, item := range items {
        go func() {
            // In older Go, this prints "c" three times.
            // Go 1.22+ makes this a compile error.
            fmt.Println(item)
        }()
    }
}

In Go versions before 1.22, this code prints c three times. The goroutines start after the loop finishes. The variable item holds the last value. Go 1.22 changed this. The compiler now rejects the program with loop variable item captured by func literal. You must create a local copy or pass the value as an argument.

package main

import "fmt"

func main() {
    items := []string{"a", "b", "c"}

    for _, item := range items {
        // Create a local copy for the closure.
        // The closure captures this copy, not the loop variable.
        go func(val string) {
            fmt.Println(val)
        }(item)
    }
}

Passing the value as an argument fixes the capture. The closure gets its own parameter. The parameter is initialized with the current value of item. This works in all Go versions.

Shadowing error is another pitfall. error is a predeclared identifier. You can shadow it. If you do, you lose the error interface type.

package main

func main() {
    // 'error' is a predeclared identifier.
    // Shadowing it hides the error interface type.
    error := "not an error type"
    var err error = error // Error: cannot use error (untyped string constant) as error value in variable declaration
}

The compiler complains with cannot use error (untyped string constant) as error value in variable declaration. The name error now refers to the string, not the interface. The assignment fails. Avoid shadowing error.

When to use what

Use keywords to structure your code. Use func to define functions. Use if to control flow. Use for to loop. Use go to spawn goroutines. Use defer to schedule cleanup. Use select to multiplex channels. Use range to iterate. Keywords are the syntax. You cannot avoid them.

Use predeclared identifiers for standard types and values. Use int for integers. Use string for text. Use error for error handling. Use nil for zero values. Use true and false for booleans. These names are part of the language contract.

Avoid shadowing predeclared identifiers. The compiler allows it, but your teammates will not. Shadowing creates confusion. It breaks expectations. It leads to subtle bugs. Keep the standard names standard.

Use the underscore to discard values intentionally. result, _ := ... says you considered the second return value and chose to drop it. Use it sparingly with errors. Dropping an error silently is usually a mistake. if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible.

Trust gofmt to handle spacing around keywords. gofmt is mandatory. Do not argue about indentation. Let the tool decide. Most editors run it on save. Argue logic, not formatting.

Accept interfaces, return structs. This is the most common Go style mantra. Functions should accept interface parameters to allow flexibility. Functions should return concrete structs to hide implementation details. Keywords like interface and struct support this pattern.

Do not pass a *string. Strings are already cheap to pass by value. They are immutable. Passing a pointer adds indirection without benefit. Use string directly.

Context is plumbing. Run it through every long-lived call site. The context package is not a keyword, but context.Context appears in almost every function signature. It carries deadlines, cancellation signals, and request-scoped values. Always put it as the first parameter. Name it ctx.

Where to go next

Go has 25 keywords. Memorize them. You will use them every day. Predeclared identifiers are suggestions, not laws. Shadowing them is legal but confusing. The compiler is strict about keywords. It is strict for a reason. Trust the small vocabulary. It makes Go readable.