GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoPackages & Modules
Lesson 15 of 18 min
Chapter 9 · Lesson 1

Package System

Package System

Every Go source file belongs to a package. Packages are the unit of code organisation and encapsulation in Go.

Package Declaration The first line of every .go file (after optional build constraints) declares its package:

go
package mypackage

All files in the same directory must use the same package name. The one exception is test files, which may use package mypackage_test for black-box testing.

Exported vs Unexported Names Go's access control is based entirely on capitalisation:

  • Exported (public): identifiers starting with an uppercase letter (MyStruct, Println, MaxRetries)
  • Unexported (private): identifiers starting with a lowercase letter (myHelper, internalCache)

This rule applies to functions, types, variables, constants, struct fields, and interface methods. There are no public/private/protected keywords.

The init() Function Each package can define one or more init() functions:

go
func init() {
    // initialisation logic
}

init functions run automatically after all variable initialisations in the package, and before main(). Their execution order within a package follows source file order, then declaration order. They cannot be called explicitly. Common uses: registering drivers, validating configuration, setting up global state.

Blank Identifier for Side Effects Import a package purely for its init() side effects using the blank identifier:

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

Without using any exported name, the blank import prevents the "imported and not used" compile error.

Package Aliases Rename a package at the import site to avoid conflicts or improve readability:

go
import (
    mrand "math/rand"
    crand "crypto/rand"
)

Internal Packages An internal directory restricts import visibility. A package at a/b/internal/c can only be imported by code rooted at a/b. This is enforced by the Go toolchain and helps maintain API boundaries within large repositories.

Circular Imports Go does not allow circular import dependencies. If package A imports B and B imports A, the build fails. Resolve cycles by extracting shared types to a third package that both can import, or by passing values through interfaces.

Package Naming Conventions

  • Short, lowercase, single-word names preferred: fmt, http, sync
  • Avoid underscores or mixed caps in package names
  • The package name should describe what it provides, not what it contains (bytes not byteutils)

Code Examples

Exported vs unexported identifiersgo
package main

import "fmt"

// Exported: accessible from other packages
type Config struct {
    Host    string
    Port    int
    timeout int // unexported field — only accessible in this package
}

// Exported constructor returning exported type
func NewConfig(host string, port int) *Config {
    return &Config{
        Host:    host,
        Port:    port,
        timeout: 30, // set internally; caller cannot set this directly
    }
}

// Exported method
func (c *Config) Address() string {
    return fmt.Sprintf("%s:%d", c.Host, c.Port)
}

// unexported helper — not visible to other packages
func validate(c *Config) bool {
    return c.Host != "" && c.Port > 0 && c.Port <= 65535
}

func main() {
    cfg := NewConfig("localhost", 8080)
    fmt.Println("Address:", cfg.Address())
    fmt.Println("Valid:", validate(cfg))

    // cfg.timeout would be accessible here (same package)
    fmt.Println("Timeout:", cfg.timeout)

    // Exported fields can be set directly
    cfg.Host = "example.com"
    fmt.Println("Updated address:", cfg.Address())
}

Capitalisation controls visibility. The timeout field is set by the constructor but cannot be accessed by external packages. validate() is package-private. This is Go's entire access control system.

init() function and execution ordergo
package main

import "fmt"

// Package-level variables are initialised first
var globalDB = initDB()

func initDB() string {
    fmt.Println("1. initDB() called — package var init")
    return "db_connection"
}

// init() runs after package-level vars, before main()
func init() {
    fmt.Println("2. first init() — could register drivers, validate config")
}

func init() {
    // Multiple init() functions are allowed in one file
    fmt.Println("3. second init() — runs after first init()")
    if globalDB == "" {
        panic("database not initialised")
    }
}

func main() {
    fmt.Println("4. main() — all init()s have already run")
    fmt.Println("DB:", globalDB)
}

Package-level variable initialisers run first, then all init() functions in source order, then main(). Multiple init() functions in the same file are permitted and run top to bottom.

Blank imports, package aliases and internal patterngo
package main

import (
    "fmt"
    "math/rand"

    // Alias to disambiguate two packages with the same base name
    mrand "math/rand"

    // Blank import: import purely for side-effects of init()
    // In a real program you would use:
    //   _ "image/png"      — registers PNG decoder
    //   _ "github.com/lib/pq" — registers PostgreSQL driver
)

// Simulate a self-registering plugin pattern (like database drivers)
type Plugin interface {
    Name() string
}

var registeredPlugins []Plugin

func RegisterPlugin(p Plugin) {
    registeredPlugins = append(registeredPlugins, p)
    fmt.Println("registered plugin:", p.Name())
}

type jsonPlugin struct{}

func (j jsonPlugin) Name() string { return "json" }

func init() {
    // A plugin package would call Register in its own init()
    RegisterPlugin(jsonPlugin{})
}

func main() {
    // Using package alias
    n := mrand.Intn(100)
    _ = rand.Intn(100) // rand and mrand refer to the same package here
    fmt.Println("random number:", n)

    fmt.Println("plugins:", len(registeredPlugins))
    for _, p := range registeredPlugins {
        fmt.Println(" -", p.Name())
    }
}

init() in a plugin package registers the plugin before main() runs — the blank import pattern. Package aliases resolve name conflicts at the import site. The blank identifier suppresses 'imported and not used' errors.

Quick Quiz

1. How does Go determine whether a name is exported (public)?

2. When does an init() function run?

3. What is the purpose of `import _ "some/package"`?

4. What happens if package A imports package B and package B imports package A?

Was this lesson helpful?

PreviousNext