Polyglot
Languages Go scope
Go § scope

Packages and scope

Go’s scoping unit is the package. Each source file declares its package; all files in the same directory must declare the same package, and together they constitute one logical compilation unit. Scoping rules are simple: identifiers are visible from their declaration to the end of the enclosing block. Package-level identifiers visible from other packages are exported by capitalisation — uppercase initial letter is exported, lowercase is package-private. The convention eliminates the need for public/private keywords. The import mechanism admits referring to other packages; the module (declared in go.mod) admits versioned dependency management. The combination — directory-as-package, capitalisation-based exports, explicit imports, module-based dependency management — is the substance of Go’s organisational model.

Packages

Every Go source file begins with a package declaration:

package mypackage

import "fmt"

func Hello() {
    fmt.Println("Hello from mypackage")
}

A package corresponds to a directory; all .go files in that directory must declare the same package name. The package name conventionally matches the last component of its import path:

github.com/user/myproject/
├── go.mod                                         (module: github.com/user/myproject)
├── main.go                                        (package main)
└── internal/
    ├── auth/
    │   ├── auth.go                               (package auth)
    │   └── token.go                              (package auth)
    └── store/
        └── store.go                              (package store)

The main package is special — it produces an executable binary. Other packages produce libraries.

The package as scope

All top-level identifiers in a package share a single scope:

// file: a.go
package mypkg

var counter int                                   // package-level

func Increment() {
    counter++                                     // visible
}
// file: b.go
package mypkg

func Decrement() {
    counter--                                     // also visible — same package
}

There is no need for explicit “internal” cross-file linkage; all files in a package see all package-level identifiers.

Exports by capitalisation

The conventional Go visibility rule:

  • Uppercase initial letterexported (visible from other packages).
  • Lowercase initial letterunexported (visible only within the declaring package).
package mypkg

var Counter int                                   // exported
var counter int                                   // unexported

func Increment() {}                              // exported
func helper() {}                                 // unexported

type Config struct {                              // exported type
    Host    string                                // exported field
    timeout int                                   // unexported field — invisible from outside
}

The mechanism is enforced by the language; there are no public/private/internal keywords. The conventional discipline is to keep the public surface small and use unexported helpers freely.

Block scope

Identifiers within a block (any {}-delimited region) are visible from declaration to the end of the block:

func example() {
    x := 5
    {
        y := 10
        fmt.Println(x, y)                        // both visible
    }
    // fmt.Println(y)                            // ERROR: y not in scope
}

The if, for, and switch statements admit initialisation clauses introducing variables scoped to the construct:

if v := compute(); v > 0 {                       // v scoped to if + else
    fmt.Println(v)
} else {
    fmt.Println("non-positive:", v)
}
// v not in scope here

for i := 0; i < 10; i++ {                        // i scoped to the loop
    // ...
}
// i not in scope here

switch n := compute(); n {                       // n scoped to the switch
case 0:
    return
case 1, 2:
    fmt.Println(n)
}

The pattern is conventional for “compute, check, use” patterns where the result is needed only within the conditional.

Variable shadowing

A variable declared in an inner scope shadows an outer variable of the same name:

x := 5
{
    x := 10                                       // shadow
    fmt.Println(x)                                // 10
}
fmt.Println(x)                                    // 5

A common pitfall in error-handling chains:

func process() error {
    err := step1()
    if err != nil {
        return err
    }

    if err := step2(); err != nil {              // shadows outer err
        return err                                // OK
    }
    // After the if, the outer err is still the value from step1
    return err
}

The go vet tool warns about shadowing in routine cases.

Imports

The import declaration admits referring to other packages:

import "fmt"                                     // standard library
import "encoding/json"                            // dotted package path

import (
    "fmt"
    "os"
    "strings"

    "github.com/user/pkg"                        // third-party module
    "example.com/myproject/internal/config"      // module-internal
)

The conventional groupings (separated by blank lines):

  1. Standard library.
  2. Third-party.
  3. Module-internal.

The goimports tool (and most editor integrations) sorts imports automatically.

Aliased imports

import (
    f "fmt"                                       // alias as f
    . "fmt"                                       // dot import — names imported into local scope
    _ "image/png"                                 // blank import — for side effects only
)

f.Println("hello")                                // via alias
Println("hello")                                  // via dot import (rare)

The conventional discipline:

  • Avoid dot imports — they obscure where names come from.
  • Use blank imports for packages that register themselves (image format support, database drivers).
  • Use aliases to disambiguate when two imported packages have the same last component:
import (
    cryptorand "crypto/rand"
    mathrand "math/rand"
)

The _ blank import

import _ "image/png"                              // registers the PNG decoder
import _ "github.com/lib/pq"                      // registers the PostgreSQL driver

The mechanism admits package side effects (typically calls to init()) without exposing the package’s exported identifiers.

The init() function

Each source file may declare an init() function — a special function called automatically when the package is loaded:

package mypkg

import "log"

var config Config

func init() {
    config = loadConfig()
    log.Println("mypkg initialised")
}

A package may have multiple init functions across multiple files; they run in the order the files are presented to the compiler (which is essentially alphabetical by filename). All init functions of imported packages run before those of the importing package.

The conventional discipline:

  • Use init for genuine initialisation that cannot be expressed as a package-level variable initialiser.
  • Avoid heavy work in init — it complicates testing and increases startup time.
  • Prefer explicit initialisation functions for non-trivial setup.

Internal packages

Subpackages under a directory named internal/ are visible only to packages whose import path is rooted at the parent of internal/:

github.com/user/myproject/
├── go.mod
├── pkg/
│   └── api.go                                   (importable from anywhere)
├── internal/
│   └── auth/
│       └── auth.go                               (importable only by myproject)
└── cmd/
    └── server/
        └── main.go                                (can import internal/auth — same parent)

The mechanism admits exposing helpers and abstractions to your own code without making them part of the public API.

Modules

A module is a collection of packages under a single root, with a go.mod file at the root:

go.mod                                           
├── module github.com/user/myproject
├── go 1.23
└── require (
        github.com/some/dep v1.2.3
        ...
    )

The module path is conventionally a URL where the source is hosted (github.com/..., gitlab.com/..., etc.); Go’s tooling resolves dependencies by fetching from those URLs.

A working directory’s module is determined by the nearest go.mod ancestor.

Treated in Modules and packaging.

The standard library import paths

Standard library packages have no domain prefix:

import (
    "fmt"
    "os"
    "strings"
    "encoding/json"
    "net/http"
    "io"
    "context"
)

Third-party packages have a domain prefix:

import (
    "github.com/gorilla/mux"
    "go.uber.org/zap"
    "google.golang.org/grpc"
)

The convention reduces ambiguity and admits the go get tooling to fetch sources directly.

Constants and iota

Constants are scoped like variables. The iota pre-declared identifier (treated in Types) admits enumeration-like definitions:

const (
    StatusActive = iota                           // 0
    StatusInactive                                // 1
    StatusBanned                                  // 2
)

const (
    KB = 1 << (10 * (iota + 1))                  // 1<<10
    MB                                            // 1<<20
    GB                                            // 1<<30
)

Common patterns

Package layout

myproject/
├── go.mod
├── go.sum
├── README.md
├── cmd/
│   └── myserver/
│       └── main.go                              (package main; entry point)
├── internal/
│   ├── auth/
│   │   └── auth.go                               (package auth)
│   └── store/
│       └── store.go                              (package store)
└── pkg/
    └── api/
        └── api.go                                (package api; public API)

The conventional structure:

  • cmd/ — entry-point binaries.
  • internal/ — private packages.
  • pkg/ — public packages (sometimes used; not required).
  • Module-root files (go.mod, go.sum, README.md).

Exported package interface

// In package mypkg/api.go:
package mypkg

// Public surface:
type Server struct {
    addr string                                   // unexported field
}

func New(addr string) *Server {
    return &Server{addr: addr}
}

func (s *Server) Start() error {
    return s.start()
}

// Private helpers:
func (s *Server) start() error {
    // ...
}

func helper() string {
    // ...
}

The discipline keeps the public API small; helpers and internal state are unexported.

Avoiding circular imports

Go does not admit circular imports. If package a imports b, then b cannot import a, even transitively. The conventional defences:

  • Move shared types to a separate package (often called types, core, or model).
  • Use interfacesb defines an interface; a provides the implementation; b consumes the interface without importing a.
  • Restructure — circular imports often indicate that the boundary is in the wrong place.

Init for registration

package myformat

import "encoding/registry"

func init() {
    registry.Register("myformat", parse, format)
}

The pattern is conventional for plug-in style architectures (image formats, database drivers, file format support).

A note on the conventional discipline

The contemporary Go scope advice:

  • One package per directory — the language requires it.
  • Capitalise to export — the convention is enforced.
  • Group imports — standard library, third-party, internal.
  • Use internal/ for private packages.
  • Avoid circular imports — restructure or use interfaces.
  • Use init sparingly — prefer explicit initialisation.
  • Keep the public API small — unexport helpers freely.
  • Use modulesgo.mod is the boundary.

The combination — directory-as-package, capitalisation-based exports, explicit imports with grouped layout, modules and internal/ for boundaries, init for plug-in registration — is the substance of Go’s package and scope model. The discipline produces clear, navigable code with substantial structural guidance.