Skip to content

Packages and Imports Beginner

Introduction

Every Go file belongs to a package. Packages are Go's unit of code organization, compilation, and encapsulation. The capital letter export rule controls visibility — no public/private keywords. Understanding packages, imports, and the standard library is fundamental to writing and reading any Go code.

Syntax & Usage

Package Declaration

Every Go file starts with a package statement. Files in the same directory must share the same package name.

package main    // executable -- must have func main()

package user    // library package -- importable by others

package user_test // test package -- can access only exported members

package main

Only package main with a func main() produces an executable binary. Everything else is a library.

Import Paths

// Single import
import "fmt"

// Grouped imports (preferred style)
import (
    "context"
    "fmt"
    "strings"

    "github.com/google/uuid"

    "mycompany.com/myapp/internal/auth"
)

Convention: group imports in three blocks separated by blank lines:

  1. Standard library
  2. Third-party packages
  3. Internal/project packages

Import Variants

import (
    "fmt"                          // standard import

    str "strings"                  // alias -- use as str.Contains()

    . "math"                       // dot import -- Sqrt() instead of math.Sqrt()

    _ "github.com/lib/pq"         // blank import -- side effects only (init)

    _ "image/png"                  // registers PNG decoder
)
Import Form Syntax Use Case
Standard "fmt" Normal usage
Alias str "strings" Name conflicts, long paths
Dot import . "math" Avoid — pollutes namespace
Blank import _ "pkg" Run init() for side effects only

Avoid dot imports

Dot imports (." pkg") make it unclear where identifiers come from. The only acceptable use is in test files for frameworks like Ginkgo/Gomega. Standard Go code should never use them.

The Export Rule: Capital Letter = Public

package user

type User struct {      // exported -- accessible outside package
    Name  string        // exported field
    email string        // unexported -- lowercase, package-private
}

func NewUser() *User {  // exported function (constructor)
    return &User{}
}

func validate() bool {  // unexported -- internal helper
    return true
}

This applies to everything: types, functions, methods, struct fields, constants, variables.

// From another package:
u := user.NewUser()
u.Name = "Alice"      // OK -- exported
// u.email = "..."    // compile error -- unexported field

Creating Your Own Packages

myapp/
├── go.mod
├── main.go              // package main
├── config/
│   └── config.go        // package config
├── internal/
│   └── auth/
│       └── auth.go      // package auth (restricted access)
└── pkg/
    └── validator/
        └── validator.go // package validator (public API)
// config/config.go
package config

type AppConfig struct {
    Port    int
    DBHost  string
}

func Load() (*AppConfig, error) {
    // load from env/file
    return &AppConfig{Port: 8080}, nil
}
// main.go
package main

import "mycompany.com/myapp/config"

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(cfg.Port)
}

Internal Packages

The internal directory restricts who can import the package. Only the parent tree can access it.

myapp/
├── internal/
│   └── auth/          // only myapp and its children can import
└── cmd/
    └── server/
        └── main.go    // can import myapp/internal/auth

Any package outside myapp/ that tries to import myapp/internal/auth gets a compile error. The compiler enforces this — it's not just convention.

Import Cycles Are Forbidden

// package a imports package b
// package b imports package a
// COMPILE ERROR: import cycle not allowed

Solutions:

  1. Extract shared types into a third package
  2. Use interfaces — depend on behavior, not implementation
  3. Restructure — the cycle usually reveals a design problem

The init() Function

package mypackage

func init() {
    // runs automatically when package is imported
    // before main() executes
    // multiple init() per file allowed (but avoid)
}

Use init() sparingly

init() runs implicitly, making code harder to test and reason about. Prefer explicit initialization in main(). Acceptable uses: registering database drivers, codecs.

go doc

go doc fmt              # package-level docs
go doc fmt.Println      # function docs
go doc -all fmt         # everything in the package

Standard Library Highlights

Package Purpose Key Functions/Types
fmt Formatted I/O Println, Printf, Sprintf, Errorf
strings String manipulation Contains, Split, Join, Replace, TrimSpace
strconv String conversions Atoi, Itoa, ParseFloat, FormatInt
os OS interaction Open, Create, Getenv, Args, Exit
io I/O interfaces Reader, Writer, Copy, ReadAll
net/http HTTP client/server Get, ListenAndServe, HandlerFunc
encoding/json JSON encoding Marshal, Unmarshal, NewEncoder, NewDecoder
context Cancellation/deadlines Background, WithTimeout, WithCancel
sync Synchronization Mutex, WaitGroup, Once, Map
errors Error utilities New, Is, As, Unwrap
log Logging Println, Fatal, SetFlags
time Time and duration Now, Since, Sleep, After, Tick
sort Sorting Strings, Ints, Slice, Search
math Math functions Max, Min, Abs, Sqrt, Pow
regexp Regular expressions Compile, MatchString, FindString
path/filepath File path manipulation Join, Base, Dir, Ext, Walk
testing Test framework T, B, Run, Errorf, Fatal
bytes Byte slice operations Buffer, Contains, Join, NewReader

Quick Reference

Concept Rule
Executable package main + func main()
Export Uppercase first letter = exported
Unexported Lowercase first letter = package-private
Import groups stdlib → third-party → internal
Blank import _ "pkg" — side effects only
Alias alias "pkg/path" — resolve conflicts
Internal internal/ directory — compiler-enforced access restriction
Import cycles Forbidden — compile error
init() Auto-runs on import — use sparingly
Unused imports Compile error — no exceptions

Best Practices

  1. Package names are lowercase, single-worduser not userService or user_service.
  2. Don't stutteruser.User is fine, user.UserService stutters. Prefer user.Service.
  3. Keep packages focused — a package should have one clear responsibility.
  4. Use internal/ to prevent external packages from depending on your implementation details.
  5. Group imports in the standard three-block format — goimports does this automatically.
  6. Avoid init() for anything with side effects beyond registration.

Common Pitfalls

Unused imports are compile errors

Go refuses to compile with unused imports. Use the blank identifier during development:

import (
    "fmt"
    _ "os" // keep temporarily during development
)
Better: use goimports to auto-manage imports.

Circular imports

If package A imports B and B imports A, the compiler rejects it. This often means your package boundaries are wrong. Extract shared types into a separate package or use interfaces.

Unexported fields in JSON

type Response struct {
    status int    // unexported -- json.Marshal IGNORES this
    Data   string // exported -- included in JSON output
}
// {"Data":"hello"} -- status is missing

Package name vs directory name

The package name in the source code doesn't have to match the directory name, but by convention it should. Mismatches confuse everyone.

Interview Tips

Interview Tip

"How does Go handle visibility/access control?" Go uses the capital letter rule: identifiers starting with an uppercase letter are exported (public), lowercase are unexported (package-private). There is no protected — the only boundary is the package. This is enforced by the compiler, not by convention.

Interview Tip

"What is a blank import and when would you use one?" A blank import (_ "pkg") imports a package solely for its init() side effects without using any of its exported names. Common examples: database drivers (_ "github.com/lib/pq") and image format decoders (_ "image/png").

Interview Tip

"How do you avoid import cycles?" Extract shared types into a separate package, define interfaces at the consumer side rather than the provider, or restructure your package hierarchy. Import cycles are a compile error in Go and often indicate a design issue.

Key Takeaways

  • Every Go file belongs to a package; package main is the entry point.
  • Capital letter = exported, lowercase = unexported — no keywords needed.
  • Import groups: stdlib, third-party, internal — goimports enforces this.
  • internal/ directories provide compiler-enforced encapsulation.
  • Import cycles are compile errors — design around them with interfaces.
  • The standard library is extensive — learn fmt, strings, os, io, net/http, encoding/json, context, and sync first.
  • Unused imports don't compile — Go enforces clean code at the language level.