Keywords #

Go has only 25 keywords — one of the fewest among modern programming languages. Python has 35, Java has 67, C++ has more than 90. This isn’t a coincidence. Go’s designers believe a simple language produces consistent, readable, easier-to-learn code. Every keyword in Go has a strong reason for existing; every keyword that’s absent also has an equally strong reason for its absence. Understanding all 25 Go keywords means understanding the foundation of the language itself.

All 25 Go Keywords #

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

Of these 25 keywords, most have already been covered in previous articles in their respective contexts. This article collects them all in one place, groups them by function, and discusses nuances you might have missed.


Group 1: Declarations #

Keywords used to declare new identities.

package #

Every Go file must start with a package declaration. This determines which package the file belongs to. The main package is the only one that produces an executable:

package main    // executable
package config  // library
package utils   // library

import #

Includes another package for use in this file. Unused imports cause a compile error — Go doesn’t allow dead imports:

import "fmt"  // single import

import (      // grouped import — idiomatic
    "fmt"
    "os"
    "strings"

    "github.com/gin-gonic/gin"

    "myapp/config"
)

var #

Declares a variable. Can be at the package level or inside functions:

var x int                    // zero value: 0
var name string = "Budi"     // with an explicit value
var active = true            // type inference
var (                        // var block
    host = "localhost"
    port = 5432
)

const #

Declares a constant evaluated at compile time. Can’t be assigned a runtime function result:

const pi = 3.14159
const maxRetry = 3
const (
    StatusOK  = 200
    StatusErr = 500
)
const (
    Read  = 1 << iota  // iota is only valid inside a const block
    Write              // 2
    Admin              // 4
)

type #

Defines a new type. Can be based on an existing type or define structs and interfaces:

type Celsius float64               // type definition
type UserID int64                  // a meaningful type
type Handler func(http.ResponseWriter, *http.Request)  // function type

type User struct {                 // struct type
    ID   UserID
    Name string
}

type Stringer interface {          // interface type
    String() string
}

type StringMap = map[string]string // type alias (= means alias, not a new definition)

func #

Defines a function or method:

func greet(name string) string {           // a regular function
    return "Hello, " + name
}

func (u User) String() string {            // a method with a value receiver
    return u.Name
}

func (u *User) SetName(name string) {      // a method with a pointer receiver
    u.Name = name
}

func divide(a, b float64) (float64, error) { // multiple return values
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Group 2: Control Flow #

Keywords that control the order of program execution.

if and else #

Conditional branching. No parentheses needed around the condition, but curly braces are mandatory:

if x > 0 {
    fmt.Println("positive")
} else if x < 0 {
    fmt.Println("negative")
} else {
    fmt.Println("zero")
}

// if with an initializer — very idiomatic in Go
if err := doSomething(); err != nil {
    return err
}

for #

The only loop keyword in Go. Replaces while, do-while, and foreach:

for i := 0; i < 10; i++ { }       // classic three-component
for condition { }                   // while-style
for { }                             // infinite loop
for i, v := range slice { }        // range over a collection
for k, v := range mapData { }      // range over a map
for v := range channel { }         // range over a channel

switch, case, and default #

Multi-value branching. No automatic fallthrough — each case stops on its own:

switch day {
case "Saturday", "Sunday":
    fmt.Println("weekend")
default:
    fmt.Println("weekday")
}

// switch without an expression — a replacement for if-else if
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

// type switch
switch v := i.(type) {
case int:
    fmt.Println("integer:", v)
case string:
    fmt.Println("string:", v)
}

break #

Stops the nearest loop or switch. With a label, it can stop an outer loop:

for i := 0; i < 10; i++ {
    if i == 5 {
        break  // exit the loop
    }
}

// break with a label
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer  // exit the outer loop
        }
    }
}

continue #

Skips the current iteration and moves to the next one. Can also use a label:

for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue  // skip even numbers
    }
    fmt.Println(i)
}

fallthrough #

Continues execution into the next case in a switch — it must be explicit, there’s no automatic fallthrough:

switch n {
case 1:
    fmt.Println("one")
    fallthrough  // continue to case 2
case 2:
    fmt.Println("two or more")
}

goto #

Jumps to a marked label. Very rarely used in modern Go — there’s almost always a cleaner alternative:

func gotoExample() {
    i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop  // jump back to the "loop" label
    }
}

goto in Go has restrictions: it can’t jump to a place that would cause an already-declared variable to be “skipped” (for safety). In real production code, goto is almost never used — for is always more expressive and easier to read.


Group 3: Functions and Goroutines #

return #

Returns a value from a function. Go supports multiple return values:

func getUser(id int) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid id")  // early return
    }
    // ...
    return user, nil  // happy path
}

// Naked return — only for short functions with named returns
func minMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // returns min and max
}

defer #

Defers a statement’s execution until the containing function finishes. Useful for resource cleanup. Multiple defers execute LIFO (last in, first out):

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // guaranteed to be called when readFile() finishes

    // process the file...
    return nil
}

func main() {
    defer fmt.Println("third")   // executed last
    defer fmt.Println("second")
    defer fmt.Println("first")  // executed first
    fmt.Println("running")
}
// Output: running, first, second, third

go #

Starts a goroutine — a function running concurrently with the calling goroutine. Goroutines are very lightweight (starting at ~2KB of stack) compared to OS threads:

go func() {
    fmt.Println("running in a new goroutine")
}()

go processRequest(req)  // run a named function as a goroutine

// Goroutines communicating via channels
results := make(chan int)
go func() {
    results <- compute()
}()
fmt.Println(<-results)

Group 4: Composite Types #

struct #

Defines a data type grouping several fields. Go’s replacement for classes:

type Point struct {
    X, Y float64
}

type Person struct {
    Name    string
    Age     int
    Address struct {   // anonymous nested struct
        Street string
        City   string
    }
}

// Struct embedding
type Employee struct {
    Person        // embedded — Person's fields and methods are directly available
    Department string
    Salary    float64
}

interface #

Defines a behavioral contract — a collection of method signatures. Implementation is implicit (no implements declaration needed):

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader              // interface composition
    Writer
}

// Empty interface — can hold any type
var anything interface{} = 42
var anything2 any = "hello"  // any is an alias for interface{} since Go 1.18

map #

The built-in hash map data type. Keys must be comparable:

m := map[string]int{"one": 1, "two": 2}
m2 := make(map[string][]string)

// map as a set
seen := map[string]struct{}{}

chan #

Defines the channel type — a communication conduit between goroutines:

ch := make(chan int)         // unbuffered channel
bch := make(chan int, 10)    // buffered channel, capacity 10
rch := make(<-chan int)      // receive-only channel
sch := make(chan<- int)      // send-only channel

// Send and receive
ch <- 42        // send
v := <-ch       // receive
v, ok := <-ch   // receive with a check whether the channel is still open

Group 5: Memory Allocation #

new #

Allocates memory for type T, initializes it with the zero value, returns *T:

p := new(int)        // a *int pointing to 0
s := new(string)     // a *string pointing to ""
u := new(User)       // a *User with all fields at zero values

// Equivalent to:
var x int
p2 := &x

In practice, new is rarely used for structs — &User{} is more common because you can fill fields immediately.

make #

Creates and initializes a slice, map, or channel. Not the same as newmake returns a value (not a pointer) that’s ready to use:

s := make([]int, 5)         // slice len=5, cap=5
s2 := make([]int, 0, 100)   // slice len=0, cap=100
m := make(map[string]int)   // map ready to use
m2 := make(map[string]int, 50)  // map with a capacity hint of 50
ch := make(chan int)         // unbuffered channel
ch2 := make(chan int, 10)    // buffered channel
new vs make:

  new(T)  → allocates T, returns *T (pointer to the zero value)
            applies to ALL types

  make(T) → creates and initializes T, returns T (not a pointer)
            ONLY for slices, maps, and channels
            The result is ready to use (internal state initialized)

Group 6: Concurrency #

select #

Like switch but for channel operations. Waits for one of several channel operations to complete, executing the first case that’s ready:

select {
case msg := <-ch1:
    fmt.Println("from ch1:", msg)
case msg := <-ch2:
    fmt.Println("from ch2:", msg)
case ch3 <- "message":
    fmt.Println("successfully sent to ch3")
case <-time.After(5 * time.Second):
    fmt.Println("timeout!")
default:
    // non-blocking: if no channel is ready
    fmt.Println("nothing is ready")
}

A select with a default case won’t block — it goes straight to default if no channel is ready. Without default, select blocks until at least one case is ready.

range #

Iterates over a slice, array, string, map, or channel. Returns two values (index/key and value):

for i, v := range []int{1, 2, 3} { }         // slice/array
for k, v := range map[string]int{} { }        // map
for i, r := range "Hello, 世界" { }            // string (per rune)
for v := range channel { }                     // channel (until closed)
for i := range slice { }                       // index only

// Blank identifier to ignore one of them
for _, v := range slice { }   // ignore the index
for i := range slice { }      // ignore the value (shorthand)

Keywords Go Deliberately Doesn’t Have #

This is just as important as understanding the keywords it does have. Go explicitly removes several keywords common in other languages:

NOT in GoWhy?
classReplaced by struct + methods. More explicit, no inheritance hierarchy needed.
extends / implementsInterface satisfaction is implicit. No formal declaration needed.
public / private / protectedVisibility is determined by the first letter (upper/lowercase): exported vs unexported. More concise.
abstract / virtualGo has no inheritance, so they’re unneeded. Polymorphism is done via interfaces.
try / catch / finally / throwErrors are ordinary return values. No hidden exception mechanism.
while / do-while / foreachAll replaced by the flexible for. One keyword for every loop pattern.
staticPackage-level functions/variables already behave like “static” (no instance needed).
null / nil-keywordGo has nil but it isn’t a keyword — it’s the zero value for pointers, interfaces, slices, etc.
async / awaitConcurrency uses goroutines + channels, not the promise/future model.
this / selfReceivers are declared explicitly by the developer: func (u *User) Save() {}
** (exponentiation operator)Not a keyword but an operator — use math.Pow()
?: (ternary operator)None. Use a plain if-else that’s clearer to read.

Visual Summary of All 25 Keywords #

DECLARATIONS (6):
  package   → declares the package of this file
  import    → includes another package
  var       → declares a variable
  const     → declares a constant (compile-time)
  type      → defines a new type
  func      → defines a function or method

CONTROL FLOW (10):
  if        → conditional branching
  else      → the alternative branch of if
  for       → looping (the only one in Go)
  switch    → multi-value branching
  case      → a case in switch or select
  default   → the default case in switch/select
  break     → stop a loop or switch
  continue  → skip the current iteration
  fallthrough → continue to the next case (switch)
  goto      → jump to a label (rarely used)

FUNCTIONS & GOROUTINES (3):
  return    → return a value from a function
  defer     → defer execution until the function finishes
  go        → start a new goroutine

COMPOSITE TYPES (4):
  struct    → a type with a collection of fields
  interface → a behavioral contract (a collection of methods)
  map       → a key-value hash map type
  chan      → a channel type for goroutine communication

ALLOCATION (2):
  new       → allocate the zero value, return a pointer
  make      → create & initialize a slice/map/chan

CONCURRENCY (2):
  select    → wait for channel operations
  range     → iterate over collections

Complete Example Program #

The following program uses almost every Go keyword in one cohesive context — a simple concurrent task queue system:

package main  // keyword: package

import (      // keyword: import
    "fmt"
    "sync"
    "time"
)

// keyword: type, struct
type TaskStatus int

const ( // keyword: const
    StatusPending TaskStatus = iota
    StatusRunning
    StatusDone
    StatusFailed
)

func (s TaskStatus) String() string { // keyword: func
    switch s { // keyword: switch
    case StatusPending:  // keyword: case
        return "Pending"
    case StatusRunning:
        return "Running"
    case StatusDone:
        return "Done"
    default: // keyword: default
        return "Failed"
    }
}

// keyword: type, struct
type Task struct {
    ID      int
    Name    string
    Status  TaskStatus
    Result  string
    fn      func() (string, error)
}

// keyword: type, interface
type Queue interface {
    Submit(name string, fn func() (string, error)) int
    Wait()
    Results() []Task
}

// keyword: type, struct
type WorkerPool struct {
    tasks    []Task      // keyword: var not explicit (struct field)
    taskCh   chan Task   // keyword: chan
    resultCh chan Task
    wg       sync.WaitGroup
    mu       sync.Mutex
    nextID   int
}

// keyword: func
func NewWorkerPool(workers int) *WorkerPool {
    // keyword: var
    var p = &WorkerPool{
        taskCh:   make(chan Task, 100), // keyword: make
        resultCh: make(chan Task, 100),
    }

    // Run the workers — keyword: for, go
    for i := 0; i < workers; i++ {
        p.wg.Add(1)
        go func() { // keyword: go
            defer p.wg.Done() // keyword: defer
            for task := range p.taskCh { // keyword: for, range
                task.Status = StatusRunning

                result, err := task.fn()
                if err != nil { // keyword: if
                    task.Status = StatusFailed
                    task.Result = err.Error()
                } else { // keyword: else
                    task.Status = StatusDone
                    task.Result = result
                }

                p.resultCh <- task
            }
        }()
    }

    // Collect results in a separate goroutine
    go func() {
        p.wg.Wait()
        close(p.resultCh)
    }()

    return p // keyword: return
}

func (p *WorkerPool) Submit(name string, fn func() (string, error)) int {
    p.mu.Lock()
    defer p.mu.Unlock()

    p.nextID++
    task := Task{
        ID:     p.nextID,
        Name:   name,
        Status: StatusPending,
        fn:     fn,
    }
    p.tasks = append(p.tasks, task)
    p.taskCh <- task
    return task.ID
}

func (p *WorkerPool) Wait() {
    close(p.taskCh)

    // Collect all results — keyword: for, range, select
    done := make(chan struct{})
    go func() {
        for result := range p.resultCh {
            p.mu.Lock()
            for i := range p.tasks {
                if p.tasks[i].ID == result.ID {
                    p.tasks[i] = result
                    break // keyword: break
                }
            }
            p.mu.Unlock()
        }
        close(done)
    }()

    // Wait with a timeout — keyword: select
    select {
    case <-done:
        // finished normally
    case <-time.After(30 * time.Second):
        fmt.Println("Timeout waiting for tasks")
    }
}

func (p *WorkerPool) Results() []Task {
    p.mu.Lock()
    defer p.mu.Unlock()

    // Copy the slice with a new allocation — keyword: new not used here
    // but use make for a new slice
    result := make([]Task, len(p.tasks))
    copy(result, p.tasks)
    return result
}

func main() { // keyword: func, also the entry point
    pool := NewWorkerPool(3) // 3 worker goroutines

    // Submit several tasks
    tasks := []struct {
        name string
        fn   func() (string, error)
    }{
        {"Fetch user data", func() (string, error) {
            time.Sleep(100 * time.Millisecond)
            return "42 users found", nil
        }},
        {"Process payment", func() (string, error) {
            time.Sleep(200 * time.Millisecond)
            return "Rp 5,000,000 processed successfully", nil
        }},
        {"Send email", func() (string, error) {
            time.Sleep(150 * time.Millisecond)
            return "3 emails sent", nil
        }},
        {"Backup database", func() (string, error) {
            time.Sleep(300 * time.Millisecond)
            return "Backup done: 2.3GB", nil
        }},
        {"Generate report", func() (string, error) {
            time.Sleep(250 * time.Millisecond)
            return "Q4 report ready", nil
        }},
    }

    // keyword: for, range
    for _, t := range tasks {
        id := pool.Submit(t.name, t.fn)
        fmt.Printf("Task #%d '%s' submitted\n", id, t.name)
    }

    fmt.Println("\nWaiting for all tasks to finish...")
    pool.Wait()

    fmt.Println("\n=== Task Results ===")
    // keyword: for, range
    for _, task := range pool.Results() {
        status := "✓"
        // keyword: if
        if task.Status == StatusFailed {
            status = "✗"
        }
        fmt.Printf("[%s] #%d %-25s → %s\n",
            status, task.ID, task.Name, task.Result)
    }

    // keyword: var — explicit declaration
    var total int = len(pool.Results())
    fmt.Printf("\nDone. Total tasks: %d\n", total)
}

Summary #

  • Go has only 25 keywords — deliberately few to keep the language simple and consistent.
  • 6 declaration keywords: package, import, var, const, type, func.
  • 10 control flow keywords: if, else, for, switch, case, default, break, continue, fallthrough, goto.
  • 3 function & goroutine keywords: return, defer, go.
  • 4 composite type keywords: struct, interface, map, chan.
  • 2 allocation keywords: new (pointer to the zero value, all types) and make (ready to use, slices/maps/chans only).
  • 2 concurrency keywords: select (wait on channels) and range (iterate collections).
  • goto exists but is almost never usedfor is always more expressive.
  • Absent: class, extends, implements, try/catch, while, async/await, this, the ternary ?: — all deliberately removed with strong design reasons.
  • Understanding these 25 keywords means understanding the entire foundation of Go syntax — no hidden keywords or surprises.

← Previous: Vendoring   Next: Goroutines →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact