Program Structure and Lexical Elements

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18. It explains how the compiler reads a Go source file — from the bytes of the file to tokens, declarations and blocks — and how a finished program starts and stops. Everything here is specified by the "Lexical elements", "Blocks", "Declarations and scope" and "Program initialization and execution" sections of the language specification; the page links to the exact anchors. The examples use the shelf library-catalogue scenario that the rest of this section builds on, and every one of them was compiled, vetted and run with Go 1.27.2.

Go’s lexical grammar is deliberately small: 25 keywords, a handful of operators, no preprocessor and no header files. Two rules surprise newcomers more than any others — the compiler inserts semicolons for you, and exported names are decided by the case of their first letter. Both are explained below.

Source files and packages

A Go program is made of packages, and a package is made of the .go files in one directory. Every source file has the same three-part shape, in this order:

  1. the package clause, which names the package the file belongs to;

  2. zero or more import declarations;

  3. zero or more top-level declarations of constants, types, variables and functions.

// Command shelf prints a greeting for the library catalogue.
package main

import (
	"fmt"
	"os"
)

const appName = "shelf"

var version = "0.1.0"

type Book struct {
	Title string
}

func describe(b Book) string {
	return fmt.Sprintf("%s: %q", appName, b.Title)
}

func main() {
	fmt.Println(describe(Book{Title: "Dune"}), "version", version)
	if len(os.Args) > 1 {
		fmt.Println("arguments:", os.Args[1:])
	}
}
$ go run . a b
shelf: "Dune" version 0.1.0
arguments: [a b]

Points worth knowing about this structure:

  • The package clause is mandatory and is not a declaration, so it does not take part in scope rules. All files in a directory must use the same package name (the only exception is the _test suffix of external test packages, see Testing). The name _ is not allowed.

  • Imports come before every other declaration, and an import that is not used is a compile error (see below). The package name an import introduces is scoped to the file, not the package. Import forms (alias, dot, blank) and the rules of init are detailed in Packages, Imports and Initialization.

  • After the imports, declarations may appear in any order. Unlike C, a function can be used above the line that defines it, and package-level variables are initialized by dependency, not by position (see Program initialization and execution).

  • A file may start with a build constraint such as //go:build linux. It must come before the package clause and be followed by a blank line; it is a directive, not an ordinary comment. Build constraints replaced the older // +build form in Go 1.17.

  • The file name has no meaning to the language, but the go tool uses suffixes: _test.go marks test files and _linux.go or _arm64.go constrain a file to a platform.

  • Source text is Unicode encoded as UTF-8. A leading byte order mark is ignored; the NUL character and a BOM anywhere else are rejected.

Two everyday compile errors belong to this structure. Go refuses to build a file that imports a package it does not use, and it refuses a local variable that is never used:

package main

import (
	"fmt"
	"os"
)

func main() {
	count := 3
	fmt.Println("shelf")
}
# example.com/shelf
main.go:5:2: "os" imported and not used
main.go:9:2: declared and not used: count

go vet reports the unused variable too, because the type checker is shared. Treat both as a feature: the compiler keeps the dependency list and the code honest. When you are experimenting, _ = count silences the error (see The blank identifier), and an editor with gopls or goimports adds and removes imports automatically.

A top-level name can be declared only once per package. Redeclaring it, or using := when no new variable would be created, is also an error:

package main

import "fmt"

type Book struct{ Title string }

var Book = "dune"

func main() {
	a, b := 1, 2
	a, b := 3, 4
	fmt.Println(a, b)
}
# example.com/shelf
main.go:7:5: Book redeclared in this block
	main.go:5:6: other declaration of Book
main.go:11:7: no new variables on left side of :=

The main package and func main

A package named main that declares a function main with no parameters and no results is an executable command. go run and go build turn it into a program; any other package name produces a library that other packages import. Both halves of the rule are enforced:

$ go build -o /dev/null .          # package main, but no func main
# example.com/shelf
runtime.main_main·f: function main is undeclared in the main package
$ go run .                         # package shelf with a func main: a library, not a program
package example.com/shelf is not a main package

The main function takes no arguments and returns nothing. Command-line arguments are in os.Args (index 0 is the program name), and the exit status is set with os.Exit, not with a return value. The flag package and the structured approach to commands are covered in Command-Line Programs.

Program initialization and execution

Before main runs, the runtime initializes the program in a fixed order:

  1. packages are initialized one at a time, each only after all the packages it imports; the dependency order is derived from the import graph and each package is initialized once, however many packages import it;

  2. inside a package, package-level variables are initialized in declaration order, except that a variable is initialized only after the variables it depends on (the dependency analysis follows references through functions);

  3. then every init function runs in the order the compiler is given the files (the go tool sorts file names), and within a file in source order. A file may declare several init functions, and they cannot be called or referenced;

  4. finally main.main is called.

package main

import "fmt"

var (
	a = c + b // initialized last: depends on b and c
	b = f()   // initialized after d, because f refers to d
	c = 1     // initialized first: no dependencies
	d = 3     // initialized before b, although declared after it
)

func f() int {
	fmt.Println("f called, d =", d)
	d++
	return d
}

func init() {
	fmt.Println("init #1:", a, b, c, d)
}

func init() {
	fmt.Println("init #2: a file may declare several init functions")
}

func main() {
	fmt.Println("main starts")
}
f called, d = 3
init #1: 5 4 1 4
init #2: a file may declare several init functions
main starts

The program ends when main returns — immediately. Goroutines that are still running are abandoned, and no pending defer of another goroutine runs. os.Exit ends the program at once with the given status and skips the deferred calls of the calling goroutine too:

package main

import (
	"fmt"
	"os"
)

func run() error {
	defer fmt.Println("run: deferred call runs on return")
	return fmt.Errorf("catalogue is empty")
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "shelf:", err)
		defer fmt.Println("main: this deferred call is skipped by os.Exit")
		os.Exit(2)
	}
}
run: deferred call runs on return
shelf: catalogue is empty
exit status 2

(exit status 2 is printed by go run; a compiled binary just exits with code 2.) A common structure is therefore a tiny main that calls run() error, prints the error and calls os.Exit exactly once. An unrecovered panic in any goroutine also terminates the whole program, with status 2 and a stack trace. See Error Handling and Goroutines and the Scheduler.

Source: os: Exit

Comments and directives

There are two forms of comment, both with C syntax: // runs to the end of the line, and /* ... */ runs to the closing marker and does not nest. A comment that spans lines acts like a newline, a single-line one acts like a space. Comment markers inside string, rune and raw-string literals are just characters.

// Command comments shows the three kinds of comment.
package main

import "fmt"

/*
A block comment that contains a newline acts like a newline,
a block comment on one line acts like a space.
*/

// Greet returns the greeting. Doc comments start with the name they document.
func Greet(name string) string {
	return "hello, " + /* inline */ name // line comment
}

//go:noinline
func answer() int { return 42 } // a directive has no space after the slashes

func main() {
	fmt.Println(Greet("shelf"), answer())
	s := "a /* not a comment */ b" // comment markers inside strings are literal
	r := `// nor here`
	fmt.Println(s, r)
}
hello, shelf 42
a /* not a comment */ b // nor here

Three conventions sit on top of the syntax:

  • A doc comment is the comment directly above a package, constant, type, variable or function declaration (no blank line between). It starts with the name it describes and is shown by go doc and pkg.go.dev. The syntax — paragraphs, headings, lists, links — is covered in Documentation and Doc Comments; gofmt reformats doc comments since Go 1.19.

  • A directive is a line comment that begins //go: (or //line, //export) with no space after the slashes. The compiler, linker and go tool read them; gofmt leaves them alone and they are not part of doc comments. Examples are //go:build, //go:generate, //go:embed and //go:noinline; they are listed in Directives, Code Generation and Embedding.

  • There is no way to comment out code safely with /* */ if the code contains a string with */; prefer // per line (editors do this with one keystroke).

Tokens and automatic semicolon insertion

The compiler splits the text into tokens: identifiers, keywords, operators and punctuation, and literals (integer, floating-point, imaginary, rune and string). White space — spaces, tabs, carriage returns and newlines — only separates tokens. The formal Go grammar uses a semicolon to end statements and declarations, but programs almost never write one, because the lexer inserts it:

  1. When the input is broken into tokens, a semicolon is automatically inserted at the end of a line if that line’s last token is

    • an identifier,

    • an integer, floating-point, imaginary, rune or string literal,

    • one of the keywords break, continue, fallthrough or return,

    • one of the operators and punctuation ++, --, ), ] or }.

  2. To allow complex statements to occupy a single line, a semicolon may be omitted before a closing ) or }.

The first rule explains every Go layout surprise. Consider the code below, which gofmt accepts, and the places where a semicolon is inserted:

package main

import "fmt"

// Semicolons are inserted by the lexer; this file shows where.
func main() {
	title := "Dune" // a semicolon is inserted after the string literal
	total := 1 +
		2 // no semicolon after the trailing +, so the expression continues
	books := []string{
		"Dune",
		"Emma", // the trailing comma is required before the newline
	}
	for i := 0; i < len(books); i++ {
		fmt.Println(i, books[i])
	}
	fmt.Println(title, total)
}
0 Dune
1 Emma
Dune 3

The consequences are the "style rules" that are really syntax:

  • The opening brace must be on the same line as the func, if, for, switch, select or type it belongs to. If it is on the next line, the line before it ends in ) or an identifier and gets a semicolon:

    package main
    
    import "fmt"
    
    func main()
    {
    	fmt.Println("hello")
    }
    # example.com/shelf
    main.go:6:1: syntax error: unexpected semicolon or newline before {
  • else must be on the same line as the closing brace of the if, for the same reason:

    package main
    
    import "fmt"
    
    func main() {
    	n := 3
    	if n > 2 {
    		fmt.Println("big")
    	}
    	else {
    		fmt.Println("small")
    	}
    }
    # example.com/shelf
    main.go:10:2: syntax error: unexpected keyword else, expected }
  • A multi-line composite literal or call needs a trailing comma on its last element, because the last element is followed by a newline and an identifier or literal gets a semicolon there:

    package main
    
    import "fmt"
    
    func main() {
    	books := []string{
    		"Dune",
    		"Emma"
    	}
    	fmt.Println(books)
    }
    # example.com/shelf
    main.go:8:9: syntax error: unexpected newline in composite literal; possibly missing comma or }
  • A binary operator must end the line, not start the next one. Writing the + at the start of the continuation line leaves a complete statement total := 1 on the first line, and the second line becomes a separate expression statement that the compiler rejects:

    package main
    
    import "fmt"
    
    func main() {
    	total := 1
    		+ 2
    	fmt.Println(total)
    }
    # example.com/shelf
    main.go:7:3: +2 (untyped int constant 2) is not used
  • Method chains break after the dot, not before it, for the same reason. The following compiles and prints bbnbnb:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func main() {
    	s := strings.NewReplacer("a", "b").
    		Replace("banana")
    	fmt.Println(s)
    }

Explicit semicolons are legal and are used in the three-clause for header (for i := 0; i < n; i++), in the initializer of if and switch (if v, ok := m[k]; ok), and to put several statements on one line. gofmt rewrites the latter into separate lines, which is why you rarely see them. Because the rules are mechanical, the compiler never guesses: a missing brace or comma is reported at the line where the unexpected semicolon was inserted, which is sometimes one line below where you would look.

Bodner presents the semicolon rule inside the chapter on go fmt, and Vivien (ch. 2) lists it with the lexical rules. Both are right; what changed is the tooling around it. Editors now run gofmt (through gopls) on every save, so you see the effect of an inserted semicolon immediately instead of at compile time.

Identifiers

An identifier names a variable, constant, type, function, field, method, package, or label. It is a letter followed by letters and digits, where a letter is any Unicode letter (category L) or the underscore, and a digit is any Unicode decimal digit (category Nd). There is no length limit.

package main

import "fmt"

type ISBN string

type 書籍 struct { // a Unicode letter starts an identifier, but it is not exported
	題名 string
}

func main() {
	π := 3.14159
	café := "espresso"
	ключ := 7
	b := 書籍{題名: "Dune"}
	fmt.Println(π, café, ключ, b)
	var Ä, ä = 1, 2 // Ä is upper case (Lu) so a package-level Ä would be exported
	fmt.Println(Ä, ä)
}
3.14159 espresso 7 {Dune}
1 2

Unicode identifiers are legal, but the standard library and almost all code bases use ASCII English names so that every contributor can type and search for them. Identifiers are case-sensitive: Book, book and BOOK are three different names, and the case of the first letter has a meaning, described in Exported identifiers.

Keywords

Go has 25 reserved words. They cannot be used as identifiers:

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

Using one as a name gives a syntax error at the keyword, followed by a second error because the parser loses its place:

package main

func main() {
	var func = 1
	_ = func
}
# example.com/shelf
main.go:4:6: syntax error: unexpected keyword func, expected name
main.go:6:1: syntax error: unexpected }, expected (

Notable absences are while, do, class, this, try, catch, throw, new (that one is a predeclared function, not a keyword), public and private. Words such as string, len, true or nil are not keywords; they are predeclared identifiers and can be shadowed (see The universe block and predeclared identifiers). iota, any and comparable are predeclared too.

Operators and punctuation

The spec lists these tokens as operators and punctuation:

+    &     +=    &=     &&    ==    !=    (    )
-    |     -=    |=     ||    <     <=    [    ]
*    ^     *=    ^=     <-    >     >=    {    }
/    <<    /=    <<=    ++    =     :=    ,    ;
%    >>    %=    >>=    --    !     ...   .    :
     &^          &^=          ~

The meaning and precedence of the operators have their own page, Operators and Expressions. Two of the tokens are lexical oddities: ` and `--` are *statements*, not expressions (so `x = i is a syntax error), and ~ appears only in type constraints (see Generics). Literals — numbers with 0b, 0o, 0x prefixes and _ separators, runes such as 'a', interpreted strings in double quotes and raw strings in backquotes — are covered in Types and Values and Strings, Runes and UTF-8.

Blocks and scope

A block is a possibly empty sequence of declarations and statements within matching braces. Go also has implicit blocks, which is what makes its scope rules precise:

  1. the universe block encompasses all Go source text;

  2. each package has a package block containing all the Go source text for that package;

  3. each file has a file block containing the file’s source text;

  4. each if, for and switch statement is considered to be in its own implicit block (so a variable declared in the if initializer is visible in all its branches but nowhere else);

  5. each clause in a switch or select acts as an implicit block;

  6. a function body, and every explicit { ... }, is a block.

Six nested rectangles from the outside in: universe block with the predeclared identifiers, package block with package-level declarations, file block with imports, function block of main, the implicit block of an if statement, and an explicit inner block. An identifier is resolved from the innermost block outwards.

The scope of an identifier is the region of source text in which it denotes the declared entity. The rules are:

  • predeclared identifiers have universe scope;

  • an identifier declared at the top level (outside any function) has package scope — visible in every file of the package;

  • the name of an imported package has file scope;

  • a parameter, result or receiver has scope of the function body;

  • a constant or variable declared inside a function begins its scope after the declaration (so x := x + 1 refers to the outer x on the right-hand side), and a type begins its scope at its identifier (so a type can refer to itself);

  • a label has function scope, in a namespace of its own.

An identifier declared in an inner block shadows the one in the outer block for as long as the inner scope lasts:

package main

import "fmt"

var scope = "package"

func main() {
	fmt.Println(scope) // package
	scope := "function"
	fmt.Println(scope) // function
	if scope := "if"; len(scope) > 0 {
		fmt.Println(scope) // if
		{
			scope := "inner block"
			fmt.Println(scope) // inner block
		}
	}
	fmt.Println(scope) // function
	show()
}

func show() {
	fmt.Println(scope) // package
}
package
function
if
inner block
function
package

Shadowing is legal, and sometimes the point (the if v, ok := m[k]; ok idiom), but it hides the classic bug of using := where = was meant. The inner n below never touches the outer n, the program compiles only if the inner value is used, and the compiler reports the unused one:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	var n int
	if s := "42"; len(s) > 0 {
		n, err := strconv.Atoi(s) // := declares new n and err in the if block
		if err != nil {
			fmt.Println(err)
		}
	}
	fmt.Println(n)
}
# example.com/shelf
vet: main.go:11:3: declared and not used: n

(go build prints the same message without the vet: prefix.) If the inner variable is used — for example passed to a function — the bug compiles silently and the outer variable stays zero. The standard go vet does not include the shadow analyzer because it reports many intentional uses; it is available in golang.org/x/tools and through golangci-lint, and is described in Formatting, vet, fix and Static Analysis. The variables-constants-and-iota page returns to the declaration rules of var and := in detail (Variables, Constants and iota).

Bodner (ch. 4) devotes a section to blocks and shadowing and recommends the shadow analyzer; that advice is still sound. What changed is the loop variable: the v := v copy that deliberately shadowed it before a goroutine or closure is no longer needed, because since Go 1.22 each iteration of a for loop has its own variable in modules that declare go 1.22 or later.

Uniqueness and labels

A name may be declared only once in a given block, and function parameters and results are in the function’s outermost block, so func f(n int) { var n string } is a redeclaration error. Method names and struct field names live in their own namespaces and may repeat the name of a package-level identifier. Labels (for goto, break and continue) are declared only for the function body, are used only by those statements, and a declared label that is never used is a compile error (label L defined and not used).

The universe block and predeclared identifiers

The universe block holds the identifiers the language provides without an import:

Kind Identifiers

Types

any bool byte comparable complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr

Constants

true false iota

Zero value

nil

Functions

append cap clear close complex copy delete imag len make max min new panic print println real recover

Because these are identifiers and not keywords, any block may redeclare them. That is allowed and almost always a mistake:

package main

import "fmt"

func main() {
	len := 3 // shadows the builtin len
	true := false
	nil := 0
	fmt.Println(len, true, nil)
	// fmt.Println(len("abc")) would not compile now: len is an int variable
}
3 false 0

Shadowing string, error, copy, len, max or min is the usual way this goes wrong, in particular because the set has grown: any and comparable arrived with generics in Go 1.18, and min, max and clear in Go 1.21. Code written before then (and both reference books were) may use min or max as the name of its own helper functions or variables. That code still compiles, since the new names are in the universe block and are shadowed by package-level declarations, but it is a candidate for deletion in favour of the builtins. The linters predeclared and revive (redefines-builtin-id) flag the shadowing.

The blank identifier

The underscore _ is the blank identifier. It can be used like any identifier in a declaration, but it declares nothing and its value can never be read. It has four common roles:

package main

import (
	"fmt"
	"io"
	"strings"
)

type Store interface{ Len() int }

type memStore struct{ n int }

func (m *memStore) Len() int { return m.n }

// Compile-time proof that *memStore implements Store.
var _ Store = (*memStore)(nil)

func main() {
	_, err := io.Copy(io.Discard, strings.NewReader("Dune")) // ignore the byte count
	fmt.Println("err:", err)

	for range 2 { // no loop variable needed
		fmt.Print("*")
	}
	fmt.Println()

	for _, title := range []string{"Dune", "Emma"} { // ignore the index
		fmt.Println(title)
	}
}
err: <nil>
**
Dune
Emma
  1. discard a result you do not need (_, err := ..., _ = x to silence an "unused" error while debugging);

  2. skip an index or value in for ... range; with Go 1.22 for range n needs no variable at all;

  3. assert at compile time that a type implements an interface (var _ Store = (*memStore)(nil)), which costs nothing at run time;

  4. import a package only for its side effects (import _ "embed" or a database driver); the full rules are in Packages, Imports and Initialization.

It also names unused function parameters and receivers (func (*memStore) Close() error), and it is how a method set is checked in tests. Note that _ may appear several times in one declaration list, and that _ = err does not make a swallowed error acceptable: handle it, or document why it is ignored.

Exported identifiers

Go has no public or private keyword. Visibility across packages is decided by the first character of the name: an identifier is exported if its first character is a Unicode upper-case letter (category Lu) and it is declared in the package block or is a field or method name. Everything else is visible only inside its own package. The unit of encapsulation is therefore the package, not the type — two types in the same package see each other’s unexported fields.

// Package catalog holds the shelf domain types.
package catalog

// Book is exported: its name starts with an upper-case letter.
type Book struct {
	Title string // exported field
	isbn  string // unexported field, visible only inside package catalog
}

// New returns a Book. Only code inside the package can set isbn directly.
func New(title, isbn string) Book { return Book{Title: title, isbn: isbn} }

// ISBN is a getter: no "Get" prefix.
func (b Book) ISBN() string { return b.isbn }

func normalize(s string) string { return s }

From another package, the exported names work and the unexported ones do not exist:

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

func main() {
	b := catalog.New("Dune", "9780441013593")
	fmt.Println(b.Title, b.ISBN())
	// fmt.Println(b.isbn)             // compile error, see below
	// fmt.Println(catalog.normalize("x"))
}
Dune 9780441013593

Un-commenting the last two lines fails to compile:

main.go:11:16: b.isbn undefined (type catalog.Book has no field or method isbn)
main.go:12:22: undefined: catalog.normalize

Practical consequences:

  • Renaming an identifier from lower to Upper changes the public API of the package; do it deliberately.

  • Characters without a case (Chinese, Japanese, _) are never upper case, so a name starting with them is never exported. Wrap it in an English-named exported function if it must be visible.

  • Fields that encoding/json, reflect and templates need to see must be exported: those packages live in other packages and cannot read an unexported field. This is the most common reason a struct "serializes to `{}`".

  • The internal directory adds a second visibility level between "package" and "everyone", enforced by the go tool (see Packages, Imports and Initialization).

Naming conventions

The language fixes the rules above; the community fixes the rest, and the standard library is the model. The authoritative texts are Effective Go ("Names"), the wiki page "Go Code Review Comments" and the blog post "Package names".

Element Convention Example

Package

Short, lower case, one word, no underscores or mixed case; the name is the last element of the import path; avoid util, common, base

catalog, strings, httptest

Exported / unexported

MixedCaps and mixedCaps; never snake_case or SCREAMING_CASE (constants included)

MaxLoans, defaultTimeout

Initialisms

Keep a consistent case: all upper (ID, URL, HTTP, ISBN) or all lower (id, url) depending on position

ServeHTTP, userID, baseURL, ISBN

Getters and setters

The getter is the field name without Get; the setter keeps Set

b.ISBN(), b.SetISBN(v)

Interfaces

One-method interfaces take the method name plus -er

Reader, Stringer, Closer

Receivers

One or two letters, the same on every method of the type; not this or self

func (b *Book) Title()

Errors

Sentinel variables begin Err, error types end in Error

ErrNotFound, *PathError

Scope length

Short names for short scopes (i, b, ctx), descriptive names for package-level and long-lived ones

for i, b := range books

Stutter

The package name is part of the qualified name, so do not repeat it

catalog.Store not catalog.CatalogStore

Files

Lower case, words joined with underscores; suffixes _test, _linux, _arm64 have a meaning

store_test.go, signal_unix.go

package main

import (
	"fmt"
	"net/url"
)

// ISBN is an initialism, so it stays upper case; compare Isbn or Id.
type ISBN string

// Book keeps the field names short and the initialisms consistent.
type Book struct {
	ID    int
	Title string
	isbn  ISBN
}

// ISBN is the getter for the unexported isbn field: no Get prefix.
func (b Book) ISBN() ISBN { return b.isbn }

// SetISBN is the setter: the Set prefix is kept.
func (b *Book) SetISBN(v ISBN) { b.isbn = v }

// Names that read well at the call site: strings.Builder, bytes.Buffer, url.URL.
type HTTPClient struct{ baseURL *url.URL }

func main() {
	var b Book
	b.SetISBN("9780441013593")
	userID := 42 // initialism in the middle: lower-case first word, then ID
	fmt.Println(b.ISBN(), userID, HTTPClient{}.baseURL == nil)
}
9780441013593 42 true

Underscores appear only in file names, test names such as TestStore_Lend, cgo, and generated code. The reason behind the short-names rule is that Go code is read far more often than written, and a name’s length should be proportional to the distance between its declaration and its last use. Vivien (ch. 2) and Bodner (ch. 2 and 4) both give this advice, and both remain current. Additional guidance on package names is in the blog post and in Idioms, Style and Code Review.

gofmt: one style for everyone

Go ends style debates with a tool. gofmt rewrites source into the canonical layout: tabs for indentation, spaces for alignment, a fixed placement of braces and spacing around operators, sorted imports within a block, and aligned comments and struct fields. The go fmt command runs gofmt -l -w over the packages you name. Because the format is mechanical, diffs contain only real changes and code from any author looks the same.

package main

import "fmt"

func main() {
    x := 1; y := 2
    if x<y {fmt.Println( "less" )}
}
$ gofmt -l .
main.go
$ gofmt -d main.go
--- main.go.orig
+++ main.go
@@ -3,6 +3,9 @@
 import "fmt"

 func main() {
-    x := 1; y := 2
-    if x<y {fmt.Println( "less" )}
+	x := 1
+	y := 2
+	if x < y {
+		fmt.Println("less")
+	}
 }

Source: cmd/gofmt

The flags to remember are -l (list files whose formatting differs), -d (print a diff), -w (write the result back) and -s (apply safe simplifications such as for _ = range x to for range x and []T{T{}} to []T{{}}). Spacing you see in expressions — x + y*z — is gofmt showing operator precedence, not a stylistic choice. Line length is not enforced; the usual advice is to break lines at natural boundaries rather than at a limit. Continue with Formatting, vet, fix and Static Analysis for goimports, go vet, go fix and the linters, and with Getting Started for the first run of the tools.

Where the books are outdated

The lexical rules of the language have been stable since Go 1.0, so Bodner (ch. 1, 2 and 4) and Vivien (ch. 2) remain accurate on semicolons, identifiers, scope and the blank identifier. The things to adjust when you read them:

  • Predeclared identifiers. Both books predate any and comparable (1.18) and the builtins min, max and clear (1.21); the set above is the current one. The print and println builtins exist for bootstrapping and should not be used in programs — use fmt or log/slog.

  • Loop-variable scope. Advice to copy the loop variable (v := v) so closures capture a fresh value predates Go 1.22, which made the variable per-iteration for modules with go 1.22 or later in go.mod.

  • Build constraints. The // +build form is replaced by //go:build (1.17); gofmt rewrites old lines.

  • Workspace layout. Source organised under $GOPATH/src has been replaced by modules; a package’s import path now starts with the module path from go.mod (see Modules and Dependency Management).

  • Doc comments. Their syntax was formalised in Go 1.19 and gofmt now reformats them; the older golint recommendations quoted in the books are no longer maintained — use go vet, gopls and staticcheck instead.

  • Range clauses. for range n (integers, 1.22) and range over functions (1.23) did not exist; _ in a range clause is still the way to skip a value.