Polyglot
Languages Go syntax
Go § syntax

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; main produces 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 use PascalCase or camelCase based 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/png for image format support, database/sql drivers).
  • 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 generate directive admits external code generation.
  • Conditional compilation by #ifdef — not admitted; build tags and GOOS/GOARCH filename 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.