Vee's Golang Notes

Program structure (Golang)

21 Sep 2025

Program structure (Golang)

Overview

A Go program is organized into modules, packages, and files. A module is the top-level unit (declared by go.mod) and contains one or more packages. A package groups related source files. A package built as package main with a func main() is an executable. All Go source files in the same directory must declare the same package name (with one exception: tests may use <pkg>_test).

Key terms:

  • Module — logical project set, declared with go.mod.
  • Package — collection of .go files that compile together (e.g., fmt, net/http, or mypkg).
  • File.go source file; must begin with package <name>.

Module (go.mod)

Create a module for your project. This file pins module path and dependency versions.

Example:

# initialize a module
go mod init github.com/you/projectname

go.mod example:

module github.com/you/projectname

go 1.21

require (
    github.com/some/dependency v1.2.3
)

go version and module path are used by the toolchain and package import paths.


Package and file layout

  • Every .go file starts with:
package packagename
  • Package names are short, lowercase, singular where possible.
  • package main indicates an executable package. The compiler expects func main() there.
  • Non-main packages are libraries (importable by other packages).

Example file hello.go:

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, world!")
}

If you have multiple files in the same directory, they must use the same package:

/cmd/myapp/main.go         -> package main
/internal/util/util.go     -> package util
/internal/util/parse.go    -> package util

Imports

Use imports to reference other packages.

import (
    "fmt"
    "net/http"
    "github.com/you/projectname/internal/util"
)
  • Standard library imports go first (you can group them).
  • Use fully qualified module paths for external packages.
  • The Go tool automatically fetches required modules (managed in go.mod).

The main function and init

  • func main() is program entrypoint in package main.
  • You can define multiple init() functions across files in the same package — they run before main() in unspecified order.

Example:

package main
 
import "fmt"
 
func init() {
    fmt.Println("initializing...")
}
 
func main() {
    fmt.Println("main started")
}

Order: all package-level init() run (for dependencies first), then main().


Declarations: variables, constants, types, functions

Variables

var x int = 3
var y = 4          // type inferred
z := 5             // short declaration (only inside functions)

Package-level vars:

package config
var Port = 8080

Constants

const Pi = 3.14159

Types

Define types to create clear APIs:

type User struct {
    ID   int
    Name string
}
 
type Handler func(http.ResponseWriter, *http.Request)

Functions

func Add(a, b int) int {
    return a + b
}

Exported vs unexported: Identifiers starting with an uppercase letter are exported (visible to importers). Lowercase is package-private.


Error handling

Idiomatic Go returns errors as values.

func ReadFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    return data, nil
}
 
data, err := ReadFile("foo.txt")
if err != nil {
    log.Fatalf("read failed: %v", err)
}

Do not use exceptions. Prefer explicit error returns.


Formatting, style, and tools

  • gofmt / go fmt — automatic style formatter. Always run it; CI should enforce it.
  • go vet — static analysis for suspicious code.
  • golangci-lint — optional linter aggregator for more checks.
  • Keep functions small and focused; prefer composition over heavy inheritance.

Build & run

  • Run a single file or package:
go run ./cmd/myapp
  • Build binary:
go build -o bin/myapp ./cmd/myapp
  • Test:
go test ./...
  • Tidy dependencies:
go mod tidy

A typical simple layout:

/cmd/
  myapp/
    main.go        # package main
/internal/
  app/
    app.go         # package app
  util/
    util.go
/pkg/
  shared/
    shared.go      # optional: libraries you may publish
go.mod
go.sum

/cmd/<appname> allows multiple executables. /internal packages cannot be imported outside the module (enforced by Go).


Example: Hello app with a small package

File: go.mod

module github.com/you/helloapp

go 1.21

File: cmd/hello/main.go

package main
 
import (
    "github.com/you/helloapp/greet"
)
 
func main() {
    greet.SayHello("Brian")
}

File: greet/greet.go

package greet
 
import "fmt"
 
// SayHello prints a greeting.
func SayHello(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

Run:

go run ./cmd/hello
# Output: Hello, Brian!

Multiple source files in same package

greet could be split:

greet/messages.go

package greet
 
func message(name string) string {
    return "Hello, " + name + "!"
}

greet/greet.go

package greet
 
import "fmt"
 
func SayHello(name string) {
    fmt.Println(message(name))
}

Both files compile together because they are in the same directory and package.


Visibility and API design

  • Export only what's necessary.
  • Keep types and helper functions unexported if they are internal details.
  • Use small interfaces near their use-site (interface segregation).

Example:

type Saver interface {
    Save(data []byte) error
}

Initialization patterns

Prefer explicit initialization where possible. Use init() sparingly — it makes program start behavior implicit.

Better:

func NewApp(cfg Config) (*App, error) {
    // construct and return
}

Then in main:

func main() {
    cfg := loadConfig()
    app, err := NewApp(cfg)
    if err != nil { log.Fatal(err) }
    app.Run()
}

Concurrency (brief note)

Concurrency is a core language feature with goroutines and channels. Organize concurrent code in packages that provide safe abstractions (e.g., worker pools, context cancellation).

Example:

go func() {
    // background goroutine
}()

Use context.Context to propagate cancellation.


Common conventions & best practices

  • Keep one package per directory.
  • Keep files small and cohesive.
  • Prefer composition over embedding heavy hierarchies.
  • Document exported identifiers with comments that start with the identifier name:
// Add returns the sum of a and b.
func Add(a, b int) int { ... }
  • Use go fmt & go vet in CI.
  • Use modules; avoid legacy GOPATH workflows.

Exercises (with brief answers)

  1. Exercise: Create a module github.com/you/calc with a package calc exposing Add and Mul. Then build an executable in cmd/calcapp that uses it.

    Answer (structure):

    go mod init github.com/you/calc
    /calc/calc.go         -> package calc (func Add, Mul)
    /cmd/calcapp/main.go  -> package main (imports github.com/you/calc)
    go run ./cmd/calcapp
    

    calc/calc.go:

    package calc
     
    func Add(a, b int) int { return a + b }
    func Mul(a, b int) int { return a * b }

    cmd/calcapp/main.go:

    package main
     
    import (
        "fmt"
        "github.com/you/calc"
    )
     
    func main() {
        fmt.Println(calc.Add(2,3))
        fmt.Println(calc.Mul(4,5))
    }
  2. Exercise: Show how to create and use an init() function and explain order of execution.

    Answer: init() runs before main(). If package A imports B, B’s init() runs before A’s init().

  3. Exercise: Convert a small multi-file package into a single-file package and explain visibility changes.

    Answer: Combining files into one file doesn’t change exported/unexported rules; uppercase still exports.


Extended example: small CLI that reads a file and prints word count

go.mod

module github.com/you/wc

go 1.21

cmd/wc/main.go

package main
 
import (
    "flag"
    "fmt"
    "log"
 
    "github.com/you/wc/wc"
)
 
func main() {
    path := flag.String("file", "", "file to count words in")
    flag.Parse()
 
    if *path == "" {
        log.Fatal("please supply -file")
    }
 
    count, err := wc.CountWords(*path)
    if err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Printf("Words: %d\n", count)
}

wc/wc.go

package wc
 
import (
    "bufio"
    "os"
)
 
func CountWords(path string) (int, error) {
    f, err := os.Open(path)
    if err != nil {
        return 0, err
    }
    defer f.Close()
 
    scanner := bufio.NewScanner(f)
    scanner.Split(bufio.ScanWords)
    count := 0
    for scanner.Scan() {
        count++
    }
    if err := scanner.Err(); err != nil {
        return 0, err
    }
    return count, nil
}

Build & run:

go run ./cmd/wc -file sample.txt