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 new — make 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 Go | Why? |
|---|---|
class | Replaced by struct + methods. More explicit, no inheritance hierarchy needed. |
extends / implements | Interface satisfaction is implicit. No formal declaration needed. |
public / private / protected | Visibility is determined by the first letter (upper/lowercase): exported vs unexported. More concise. |
abstract / virtual | Go has no inheritance, so they’re unneeded. Polymorphism is done via interfaces. |
try / catch / finally / throw | Errors are ordinary return values. No hidden exception mechanism. |
while / do-while / foreach | All replaced by the flexible for. One keyword for every loop pattern. |
static | Package-level functions/variables already behave like “static” (no instance needed). |
null / nil-keyword | Go has nil but it isn’t a keyword — it’s the zero value for pointers, interfaces, slices, etc. |
async / await | Concurrency uses goroutines + channels, not the promise/future model. |
this / self | Receivers 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) andmake(ready to use, slices/maps/chans only).- 2 concurrency keywords:
select(wait on channels) andrange(iterate collections).gotoexists but is almost never used —foris 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.