Syntax
The syntax of Go is defined by the Go Programming Language Specification and is deliberately small. The grammar is C-family at the surface (curly braces, function call syntax, expression operators), with substantial additions and removals: no semicolons in source (the lexer inserts them at line breaks), mandatory braces on if and for, no parentheses around conditions, declared-but-unused variables and imports are errors (not warnings), and the canonical gofmt is the language’s formatting authority. The combination — small grammar, mandatory formatting, package-based compilation, distinctive declaration order (name type) — is the substance of Go’s syntactic identity.
This page covers the surface a working programmer encounters routinely.
A complete program
The classical hello world:
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
A more substantial example:
package main
import (
"fmt"
"os"
"sort"
"strings"
)
func greet(name string) string {
return fmt.Sprintf("Hello, %s.", name)
}
func main() {
args := os.Args[1:]
if len(args) == 0 {
args = []string{"world"}
}
sort.Strings(args)
for _, name := range args {
fmt.Println(greet(strings.TrimSpace(name)))
}
}
The principal features visible:
package main— every file declares its package;mainproduces an executable.import (...)— multi-import block.func name(...) returnType { ... }— function definition.:=— short variable declaration with type inference.for _, name := range args— range loop.fmt.Println— qualified package access.
Build and execution:
go run hello.go # compile and run
go build hello.go # produce executable
./hello
go build # build a module
go install # build and install
Source character set
Go source is interpreted as UTF-8. Identifiers may use Unicode letters, though ASCII identifiers are conventional. The compiler does not normalise identifiers; two visually-distinct sequences of code points are distinct identifiers.
Identifiers and keywords
Identifiers begin with a letter or underscore and continue with letters, digits, or underscores. The convention follows the Go Style Guide:
camelCase— most identifiers.PascalCase— exported identifiers (visible from other packages).SCREAMING_SNAKE_CASE— rarely used; constants conventionally usePascalCaseorcamelCasebased on export.- Underscore prefix is not conventional in Go;
_alone is the blank identifier (discards a value).
The visibility-by-capitalisation is one of Go’s most distinctive rules: an identifier starting with an uppercase letter is exported (visible from other packages); a lowercase identifier is package-private.
package mypackage
func PublicFunction() { /* visible everywhere */ }
func privateFunction() { /* only within mypackage */ }
var ExportedVar = 1
var unexportedVar = 2
The mechanism eliminates the need for public/private keywords; the convention is enforced by the language.
The 25 reserved 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
The keyword list is deliberately small; common operations (make, new, len, cap, append, copy, panic, recover, print, println) are predeclared functions rather than keywords, technically replaceable by the user (though doing so is conventional bad form).
Comments
Two comment forms:
// A line comment, terminated by the end of the line.
/*
A block comment.
Block comments do NOT nest.
*/
Comments preceding a top-level declaration become doc comments, consumed by go doc and pkg.go.dev:
// Greet returns a greeting for the given name.
//
// If the name is empty, it returns a greeting for "world".
func Greet(name string) string {
if name == "" {
name = "world"
}
return fmt.Sprintf("Hello, %s.", name)
}
The convention: the first sentence of the doc comment begins with the identifier name. The mechanism is one of Go’s most distinctive documentation practices.
Statements vs expressions
Go is not expression-oriented (unlike Rust, Haskell, or Scala). The following are statements — they do not produce a value:
if cond { ... } else { ... }
for cond { ... }
switch x { ... }
return x
The following are expressions — they produce values:
3 + 4
f(x)
a[i]
&x
*p
Variable assignment is a statement; chained assignment (a = b = c) is not admitted. The if-as-expression form (common in Rust/Kotlin) is not available; conditional value assignment uses an explicit if/else block:
var max int
if a > b {
max = a
} else {
max = b
}
The discipline favours explicit statement-level control over compact expression chains.
Variable declarations
Three principal forms:
var x int // var with explicit type, zero value
var y int = 5 // var with explicit type and value
var z = 5 // var with inferred type
const c = 100 // compile-time constant
x := 5 // short declaration (shorthand)
a, b := 1, 2 // multiple short declaration
The := is admitted only inside functions; package-level declarations require var or const.
The zero value applies when no initialiser is given: 0 for numeric types, "" for strings, false for bool, nil for pointers/slices/maps/interfaces/channels/functions, all-fields-zero for structs.
var n int // n == 0
var s string // s == ""
var p *int // p == nil
var v []int // v == nil (nil slice is usable)
The Go convention prefers := for local declarations and var only when the explicit type is needed (or for package-level declarations).
Statement terminators
Statements are terminated by newlines. The lexer inserts implicit semicolons at the end of any line whose final token is one of:
- An identifier or literal.
- A
return,break,continue,fallthrough. - A closing
),],}, or++,--.
The discipline produces the conventional Go formatting where opening braces are on the same line as the construct:
if cond { // brace on same line
// ...
}
func foo() { // brace on same line
// ...
}
if cond // ERROR: semicolon inserted before {
{
// ...
}
The mechanism explains why opening braces must be on the same line as the introducing keyword.
Block syntax
All control-flow constructs require braces:
if cond { // braces required
doSomething()
}
if cond { doSomething() } // OK; one-liner
if cond
doSomething() // ERROR: no braces
There is no single-statement form; the mechanism eliminates the dangling-else ambiguity and produces consistent formatting.
Functions
The func keyword introduces a function:
func add(a int, b int) int {
return a + b
}
func add(a, b int) int { // shorthand for same-type params
return a + b
}
func divmod(a, b int) (int, int) { // multiple return values
return a / b, a % b
}
func parse(s string) (int, error) { // common pattern: result + error
return strconv.Atoi(s)
}
The form: func <name>(<params>) <returnTypes> { <body> }. The return types follow the parameter list (the C-style int func() is not admitted — Go reads “f is a function returning int”).
Multiple return values are first-class; the conventional Go pattern returns a result and an error.
result, err := parse("42")
if err != nil {
return err
}
Treated in Functions.
Imports
import "fmt" // single import
import ( // multi-import block
"fmt"
"os"
"strings"
)
import (
"fmt"
"github.com/user/pkg" // third-party
"example.com/internal/config" // module-internal
)
import (
f "fmt" // alias
. "fmt" // dot import (rare)
_ "image/png" // blank import (side effects)
)
The conventional discipline:
- Group standard-library imports first; third-party second; project-internal last.
- Avoid dot imports — they pollute the namespace and obscure call sites.
- Use blank imports only for packages registering themselves (
image/pngfor image format support,database/sqldrivers). - gofmt sorts the imports automatically.
The gofmt contract
The Go toolchain ships with gofmt (or go fmt) — the canonical formatter. The Go community treats gofmt output as the definitive formatting; arguments about style end at “what does gofmt do”.
The discipline produces several conventions:
- Tabs for indentation (one tab = however your editor renders it).
- Curly braces always on the same line as the opening keyword.
- One blank line between functions; multiple blank lines collapsed.
- Alignment of struct fields and consts in groups.
The conventional editor integration runs gofmt on save; CI checks that committed code is gofmt-clean.
A note on what Go admits
Several features the C-family takes for granted are absent or different:
- Inheritance — not admitted; embedding is the substitute.
- Method overloading — not admitted; functions and methods have unique names.
- Default arguments — not admitted; variadic functions or option structs are conventional.
- Generics — admitted since Go 1.18 (treated in Generics).
- Exceptions — not admitted; errors as values and panic/recover are the substitutes.
- Pointer arithmetic — not admitted; pointers may be assigned and dereferenced but not incremented.
- Implicit conversions — not admitted; numeric conversions must be explicit.
- Operator overloading — not admitted;
+,==, etc. work only on built-in types. - Macros — not admitted; the
go generatedirective admits external code generation. - Conditional compilation by
#ifdef— not admitted; build tags andGOOS/GOARCHfilename suffixes are the substitutes. - Tuples as types — not admitted; multiple return values exist but are not first-class tuples.
The combination — small grammar, mandatory formatting, no inheritance, no exceptions, explicit conversions, structural interfaces, built-in concurrency, garbage collection — is the substance of Go’s identity. The discipline of writing Go is largely the discipline of working within these constraints rather than around them.