Core Syntax #

Before writing your first line of Go, it’s worth understanding the “language within the language” — the rules that shape how every Go program is written. Some of these rules feel unique compared to other languages, and understanding why they exist (not just what they are) will make you productive much faster. This article examines the anatomy of a Go program from the top level down: how files are organized, what the mandatory components are, and why Go makes design decisions that often differ from developer expectations.

As a compiled language, Go code goes through a strict compilation stage from writing to execution to produce a self-contained binary. This code lifecycle can be visualized in the following diagram:

flowchart LR
    Src["Source Code (.go)"] --> Compiler["Go Compiler (go build)"]
    Compiler --> Exec["Executable Binary (Machine Code)"]
    Exec --> Run["Run Directly on the Target OS"]

The Three Mandatory Components of a Go Program #

Every executable Go file has three mandatory components that must appear in this order: the package declaration, imports, and the main function. Without all three, no Go program can run.

package main       // Component 1: package declaration

import "fmt"       // Component 2: import dependencies

func main() {      // Component 3: entry point
    fmt.Println("Hello, Go!")
}

This isn’t just boilerplate. Each component plays a specific role in how Go organizes and compiles code.


The Package System — Go’s Code Organization Unit #

Packages are the basic unit of code organization in Go. Every Go file must belong to a package, and every package consists of one or more .go files in a single directory.

Package Declaration #

package main        // Package that produces an executable
package mathutil    // Library package
package config      // Library package
package httpmiddleware  // Library package

main is the only package that can be executed directly — Go looks for a main() function in it as the program’s entry point. Every other package is a library that can only be imported by other packages.

Package naming conventions:

// ✓ Package names: lowercase, short, no underscores
package strings
package http
package json
package mathutil

// ✗ Avoid
package StringUtils   // no PascalCase
package math_util     // no underscores
package myVeryLongPackageName  // nothing too long

How Packages Relate to Directories #

In Go, one directory = one package. All .go files in a single directory must declare the same package (with the exception of _test files for tests).

myproject/
  ├── main.go           → package main
  ├── go.mod
  ├── config/
  │   ├── config.go     → package config
  │   └── loader.go     → package config (same!)
  ├── handler/
  │   ├── user.go       → package handler
  │   └── product.go    → package handler (same!)
  └── repository/
      ├── user_repo.go  → package repository
      └── db.go         → package repository (same!)

How to call a function from another package:

package main

import "myproject/config"

func main() {
    cfg := config.Load()  // PackageName.FunctionName
    _ = cfg
}

Imports — Using Other Packages #

import tells Go which packages this file needs. Go distinguishes three kinds of packages: the standard library, third-party modules, and internal project modules.

Single and Multiple Imports #

// Import a single package
import "fmt"

// Import multiple packages — the idiomatic way uses parentheses
import (
    "fmt"
    "math"
    "os"
    "strings"
    "net/http"
    "encoding/json"
)

The Go community strongly favors the import (...) style even for a single package, because it makes adding imports later easier without changing existing lines.

Import Order and Grouping #

The Go community convention (enforced by goimports) is to group imports into three groups separated by blank lines:

import (
    // Group 1: Standard library
    "fmt"
    "os"
    "strings"

    // Group 2: Third-party dependencies
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"

    // Group 3: Internal project packages
    "myproject/config"
    "myproject/handler"
)

Import Aliases #

Aliases are useful when two packages have a name conflict:

import (
    "fmt"
    mrand "math/rand"   // alias: call it as mrand.Intn()
    crand "crypto/rand" // alias: call it as crand.Read()
)

func main() {
    fmt.Println(mrand.Intn(100))  // random number 0-99
}

Aliases are also handy for shortening long package names:

import (
    pb "myproject/proto/generated/user/v1"
)

Blank Imports #

Sometimes you need to import a package only for the side effects of its init() function — not to call its functions directly. Use the blank identifier _:

import (
    "database/sql"
    _ "github.com/lib/pq"  // import for side effect: registers the PostgreSQL driver
)

Without _, Go errors because the pq package isn’t used explicitly. With _, Go knows it’s intentional.

Dot Imports — Avoid Them #

import . "fmt"  // every exported name from fmt is available without a prefix

Println("Hello")  // works directly, without fmt.Println

Dot imports are very rarely used in production code because they make code hard to read — it’s unclear which package a Println function comes from. The only sensible use is in test files for specific DSLs.


Syntax Rules That Can’t Be Broken #

Go has several rules that immediately cause a compile error if violated. Unlike most languages where these are just “warnings” or style violations, in Go the compiler rejects them outright. And every rule has a good reason behind it.

Rule 1: Unused Import → Compile Error #

package main

import (
    "fmt"
    "math"   // ← compile error: "math" imported and not used
)

func main() {
    fmt.Println("hello")
    // math is never used
}

Why? To keep codebases free of “dead imports” that accumulate over time and leave people wondering whether a package is still relevant. It also speeds up compilation because Go only compiles the packages that are actually used.

Rule 2: Unused Variable → Compile Error #

func main() {
    x := 10
    y := 20  // ← compile error: y declared and not used
    fmt.Println(x)
}

Why? Same reason — preventing dead code. If you declare a variable, you must use it. If not, remove it. If it’s intentionally unused (for example an irrelevant return value), use the blank identifier _.

Rule 3: Opening Brace on the Same Line #

// ✓ CORRECT: opening brace on the same line
func main() {
    if x > 0 {
        fmt.Println("positive")
    }
}

// ✗ WRONG: compile error — syntax error: unexpected newline
func main()
{
    if x > 0
    {
        fmt.Println("positive")
    }
}

Why? This relates to the Automatic Semicolon Insertion mechanism — a topic we’ll cover shortly. In short: the Go compiler automatically inserts semicolons at the end of certain lines, and if { is on a new line after func main(), the compiler inserts ; before {, producing a syntax error.

Rule 4: No Implicit Types — Be Explicit When Needed #

var i int = 10
var f float64 = i   // ← compile error: cannot use i (type int) as type float64

// Must be explicit:
var f float64 = float64(i)   // ✓

Why? Implicit conversion is one of the biggest bug sources in C. Go eliminates it entirely — all type conversions must be clearly visible in the code.


Automatic Semicolon Insertion #

Go actually uses semicolons as statement separators, but you don’t have to write them manually. The Go lexer automatically inserts a semicolon at the end of a line if the last token on that line is one of:

  • An identifier (variable, function, or type name)
  • An integer, float, imaginary, rune, or string literal
  • A keyword: break, continue, fallthrough, return
  • An operator: ++, --
  • A closing bracket: ), ], }

This explains why opening braces must be on the same line:

// Go sees this:
func main()      // ← line ends with ), so a ; is inserted!
{                // this is now like writing: func main(); {

And func main(); { is a syntax error. This is why the rule isn’t just style — it’s a technical rule directly tied to how Go parses code.


Exported vs Unexported Identifiers #

Go doesn’t use public, private, or protected keywords. Visibility is determined by the first letter of an identifier’s name — this applies to functions, types, variables, constants, and struct fields.

Capitalized first letter → Exported (accessible from other packages):

package mathutil

// Exported — can be called from other packages
func Add(a, b int) int {
    return a + b
}

type Calculator struct {
    // Exported field — accessible outside the package
    Precision int

    // Unexported field — only accessible inside package mathutil
    memory float64
}

// Exported constant
const MaxValue = 1<<31 - 1

// Exported variable
var DefaultTimeout = 30 * time.Second

Lowercase first letter → Unexported (same package only):

package mathutil

// Unexported — only callable from inside package mathutil
func validateInput(a, b int) error {
    if a < 0 || b < 0 {
        return errors.New("input must be non-negative")
    }
    return nil
}

// Unexported type
type internalState struct {
    cache map[string]int
    mu    sync.Mutex
}

// Unexported constant
const maxIterations = 1000

Using them from another package:

package main

import "myproject/mathutil"

func main() {
    result := mathutil.Add(3, 4)          // ✓ exported
    fmt.Println(result)

    // mathutil.validateInput(3, 4)       // ✗ compile error: unexported
    // mathutil.internalState{}           // ✗ compile error: unexported
}

This isn’t just access control — it’s how Go documents a package’s public API. What’s exported is what you intentionally expose. What’s unexported is implementation detail you can change anytime without affecting the package’s users.


Comments and Doc Comments #

Go has two standard comment types:

// This is a single-line comment

/*
   This is a multi-line comment.
   Can span many lines.
   Rarely used in modern Go.
*/

Doc Comments — Official Documentation #

Go has a built-in documentation system called godoc. Every exported identifier (function, type, constant, variable) should have a doc comment — a comment directly above it that starts with the identifier’s name:

// Package mathutil provides basic and advanced mathematical functions.
// This package doesn't use any third-party libraries.
package mathutil

// Add returns the sum of two integers a and b.
// There's no value limit — they can be negative or very large.
func Add(a, b int) int {
    return a + b
}

// Calculator stores calculation state and provides arithmetic operations.
// Use NewCalculator() to create a new instance.
type Calculator struct {
    // Precision determines the number of decimal digits in calculation results.
    // Default: 2
    Precision int
    memory    float64  // no doc comment for unexported fields
}

// NewCalculator creates a new Calculator with a default precision of 2.
func NewCalculator() *Calculator {
    return &Calculator{Precision: 2}
}

// ErrDivisionByZero is the error returned when the divisor is zero.
var ErrDivisionByZero = errors.New("divisor must not be zero")

With proper doc comments, you can run:

go doc mathutil.Add
# Output:
# func Add(a, b int) int
#     Add returns the sum of two integers a and b.
#     There's no value limit — they can be negative or very large.

Zero Values — Automatic Initialization #

Every variable in Go declared without an explicit value automatically gets a zero value based on its type:

var i int        // 0
var f float64    // 0.0
var b bool       // false
var s string     // "" (empty string)
var p *int       // nil
var sl []int     // nil (nil slice)
var m map[string]int  // nil (nil map)
var fn func()    // nil

This isn’t a random value from memory like in C — Go guarantees well-defined values. The implications:

// No defensive initialization needed
var counter int   // usable immediately: counter++
var messages []string  // appendable immediately

// Structs also get zero values in all their fields
type Config struct {
    Host    string
    Port    int
    Debug   bool
}

var cfg Config
// cfg.Host = ""
// cfg.Port = 0
// cfg.Debug = false

Zero values let you write cleaner code — no mandatory constructors just to initialize default values.


A More Complete Program Overview #

Here’s a program showing the various syntax elements you’ll learn in the coming articles, with annotations on each part:

// [1] Package declaration
package main

// [2] Imports grouped
import (
    "fmt"        // standard library
    "math"
    "strings"
)

// [3] Package-level constant — exported
const AppVersion = "1.0.0"

// [4] Package-level variable — unexported
var defaultGreeting = "Hello"

// [5] Custom type based on a basic type
type Celsius float64
type Fahrenheit float64

// [6] Struct with exported and unexported fields
type Person struct {
    Name  string  // exported
    Email string  // exported
    age   int     // unexported — only accessible within this package
}

// [7] Method on a struct with a pointer receiver
func (p *Person) SetAge(a int) {
    if a >= 0 && a <= 150 {
        p.age = a
    }
}

// [8] Method with a value receiver — read-only
func (p Person) Greet() string {
    return fmt.Sprintf("%s, %s! (v%s)", defaultGreeting, p.Name, AppVersion)
}

// [9] Function with multiple return values
func celsiusToFahrenheit(c Celsius) (Fahrenheit, error) {
    if c < -273.15 {
        return 0, fmt.Errorf("temperature %v is below absolute zero", c)
    }
    return Fahrenheit(c*9/5 + 32), nil
}

// [10] The main function — entry point
func main() {
    // [11] Short variable declaration
    p := Person{
        Name:  "Budi",
        Email: "[email protected]",
    }
    p.SetAge(28)

    // [12] Calling a method
    fmt.Println(p.Greet())

    // [13] Handling multiple return values + errors
    temp, err := celsiusToFahrenheit(100)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("100°C = %.1f°F\n", temp)

    // [14] The strings package — string operations
    words := strings.Fields("Go is awesome")
    for i, w := range words {
        fmt.Printf("[%d] %s\n", i, strings.ToUpper(w))
    }

    // [15] The math package
    fmt.Printf("π = %.4f\n", math.Pi)
}

Every element in this program — custom types, structs, methods, multiple return values, error handling, range — has its own article in this section covering it in depth.


Summary #

  • Three mandatory components of an executable Go program: package main, import, and func main().
  • One directory = one package — all .go files in a folder must declare the same package.
  • Unused import = compile error — Go forces code to be free of dead imports.
  • Unused variable = compile error — keeps code free of dead code.
  • The opening brace { must be on the same line — this is technical, tied to the lexer’s automatic semicolon insertion.
  • Exported vs unexported is determined by the first letter: ExportedFunc is accessible from other packages, internalFunc isn’t.
  • Doc comments start with the identifier name — used by go doc for automatic documentation.
  • Zero values guarantee every variable is initialized: 0, false, "", nil — no random values from memory.
  • Type conversion is always explicit — no silent type coercion like in JavaScript or C.

← Previous: Installation   Next: Comments →

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