Go Packages and Modules explained — CoPilot Blog
    Neura MarketNeura Market/CoPilot
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityCoPilotCoPilot
    DeepSeekDeepSeekStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityPluginsTrendingGenerate
    CoPilotBlogGo Packages and Modules explained
    Back to Blog
    Go Packages and Modules explained
    go

    Go Packages and Modules explained

    Fer Rios June 9, 2026
    0 views

    What is a package? In Go, every Go program is made up of packages. A package is a...

    ## What is a package? In Go, every Go program is made up of packages. A package is a directory of .go files that share the same package declaration. The primary purpose of packages is to help you isolate and reuse code. ```plaintext myapp/ ├── main.go ← package main └── math/ ├── add.go ← package math └── sub.go ← package math ``` Both add.go and sub.go declare package math. They can call each other's functions directly, no import needed within the same package. Inside a package, every .go file should begin with a package {name} statement which indicates the name of the package that the file is a part of. Every exported identifier (capitalized name) in that directory is accessible to anyone who imports the package. Here's what that looks like in practice: ```go // math/add.go package math // pi is an unexported variable. var pi = 3.14159 // Add returns the sum of two integers. // Exported — starts with a capital letter. func Add(a, b int) int { return a + b } ``` ```go // math/sub.go package math // Exported — starts with a capital letter. func Subtract(a, b int) int { return a-b } ``` ```go // main.go package main import ( "fmt" "github.com/yourname/myapp/math" ) func main() { fmt.Println(math.Add(3, 4)) // 7 fmt.Println(math.Subtract(10, 3)) // 7 // fmt.Println(math.pi) — compile error: unexported } ``` Two rules to remember: - Capital letter = exported (public). Lowercase = unexported (private to the package). - One package per directory. One directory per package. --- ## What is a module? If a package is a folder, a module is the whole project, a tree of packages with a name, a Go version requirement, and a list of external dependencies. When you start a Go project, you create a module, and inside that module, there will be packages. Every Go project has exactly one go.mod file at its root. That file defines the module. Here's what a real one looks like: ```go module github.com/yourname/weather-cli go 1.21 require ( github.com/aws/aws-sdk-go-v2 v1.24.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.47.0 gopkg.in/yaml.v3 v3.0.1 ) ``` Three things in every go.mod: - module — the module path. This is the base import path for every package in your project. It's usually your GitHub URL, but it can be anything. - go — the minimum Go version your code requires. - require — the external dependencies your module needs, each pinned to an exact version. ### go.sum — the lockfile Alongside go.mod lives go.sum. You never edit this by hand. It contains cryptographic checksums for every dependency (and their dependencies) your module uses: ```plaintext github.com/aws/aws-sdk-go-v2 v1.24.0 h1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= ``` go.sum guarantees that the code you download is bit-for-bit identical to what was there when you first added the dependency. It's your protection against supply chain attacks and "works on my machine" problems. Always commit both go.mod and go.sum. --- ## Working with modules day to day ### Creating a new module So, let's say you want to start a new project: you create the folder first, then you create your go.mod file, passing your module path to it (github.com/yourname/myapp). ```console mkdir myapp && cd myapp go mod init github.com/yourname/myapp ``` This creates go.mod with your module path. That's it — you're ready to write Go. ### Adding a dependency ```console go get github.com/aws/aws-sdk-go-v2/service/s3@latest ``` go get does three things: downloads the package, adds it to go.mod, and updates go.sum. After running it your go.mod will have a new require entry. You can also just write the import in your code and run go mod tidy — it figures out what's missing and adds it: ```console go mod tidy ``` go mod tidy is the command you'll run most often. It adds missing dependencies and removes unused ones, keeping go.mod clean. ### Upgrading a dependency ```console # Upgrade to the latest minor/patch version go get github.com/aws/aws-sdk-go-v2@latest # Upgrade to a specific version go get github.com/aws/aws-sdk-go-v2@v1.25.0 ``` ### Removing a dependency Remove the import from your code, then run: ```console go mod tidy ``` go mod tidy will remove the require entry automatically if nothing in your code imports it anymore. ### Viewing your dependency tree ```console go mod graph ``` This prints the full dependency graph — your dependencies, their dependencies, and so on. Useful for debugging version conflicts. ### Vendoring dependencies For environments without internet access (some CI setups, air-gapped servers), you can vendor all dependencies into a local vendor/ directory: ```console go mod vendor ``` After vendoring, go build uses vendor/ instead of the module cache. Commit vendor/ to your repo and your build never needs to call the internet. --- ## How modules and packages connect Let's make the relationship tangible. Say you run: ```shell go get github.com/spf13/cobra@v1.8.0 ``` Your go.mod now contains: ```go require ( github.com/spf13/cobra v1.8.0 ) ``` Cobra is a module at github.com/spf13/cobra. Inside that module, there are multiple packages: ```plaintext github.com/spf13/cobra ← the root package github.com/spf13/cobra/doc ← generates documentation github.com/spf13/cobra/completions ← shell completions ``` When you import github.com/spf13/cobra in your code, you're importing one specific package from that module. The module is what gets versioned and downloaded; the package is what you actually use in your code. ```go import ( "github.com/spf13/cobra" // imports the root package of the cobra module ) ``` One require in go.mod, many importable packages available. That's the module/package split. --- ## Package main is special Every Go program needs exactly one package main with exactly one main() function. That's the entry point. Everything else is a library package — it exports functions and types but can't be run directly. ```go // This is a runnable program package main import "fmt" func main() { fmt.Println("hello from main") } ``` ```go // This is a library — can't be run, only imported package greet import "fmt" func Hello(name string) string { return fmt.Sprintf("Hello, %s!", name) } ``` You can have multiple package main files in a project — but each one must live in its own directory. This is exactly what the cmd/ convention is for, which we'll get to shortly. --- ## How imports work The import path is always: module path (from go.mod) + directory path from the module root. ```plaintext module github.com/yourname/myapp ← module path myapp/ ├── go.mod └── internal/ └── config/ └── config.go ← import path: github.com/yourname/myapp/internal/config ``` ```go // Importing your own packages — always use the full module path import "github.com/yourname/myapp/internal/config" // Importing from the standard library — no module path needed import "fmt" import "net/http" import "encoding/json" // Importing a third-party package — use its full module path + sub-path import "github.com/spf13/cobra" import "github.com/aws/aws-sdk-go-v2/service/s3" ``` ### Aliasing imports When two packages have the same name, or when a name is too long, you can alias the import: ```go import ( "fmt" // Alias to avoid conflict with standard library math gomath "github.com/yourname/myapp/math" // Alias for readability yaml "gopkg.in/yaml.v3" ) func main() { fmt.Println(gomath.Add(1, 2)) _ = yaml.Marshal } ``` ### The blank identifier import Sometimes you import a package only for its side effects — database drivers are the classic example. The _ alias tells Go "import this but don't use its name": ```go import ( "database/sql" _ "github.com/lib/pq" // registers the postgres driver via init() ) ``` Without the _, Go's compiler would complain about an unused import and refuse to compile. --- ## A practical project structure Here's a real structure that scales from small to large. This is what most production Go projects converge on: ```plaintext myapp/ ├── go.mod ├── go.sum ├── Makefile │ ├── cmd/ │ ├── server/ │ │ └── main.go ← the HTTP server entry point │ └── worker/ │ └── main.go ← the background worker entry point │ ├── internal/ │ ├── handler/ │ │ ├── handler.go │ │ └── handler_test.go │ ├── store/ │ │ ├── postgres.go │ │ └── store.go │ └── config/ │ └── config.go │ └── pkg/ └── validate/ ├── validate.go └── validate_test.go ``` Let's walk through each directory. ### cmd/ — entry points One subdirectory per runnable binary. Each contains a single main.go. The main.go should do almost nothing except wire dependencies together and start the program. ```go // cmd/server/main.go package main import ( "log/slog" "net/http" "os" "github.com/yourname/myapp/internal/config" "github.com/yourname/myapp/internal/handler" "github.com/yourname/myapp/internal/store" ) func main() { cfg := config.Load() db, err := store.Connect(cfg.DatabaseURL) if err != nil { slog.Error("failed to connect to database", "error", err) os.Exit(1) } h := handler.New(db) slog.Info("starting server", "port", cfg.Port) if err := http.ListenAndServe(":"+cfg.Port, h); err != nil { slog.Error("server failed", "error", err) os.Exit(1) } } ``` > 💡No business logic in main.go. Ever. It's a wiring diagram, not an application. ### internal/ — private application code The internal/ directory is enforced by the Go toolchain. Packages inside internal/ can only be imported by code in the parent directory tree. Code outside your module cannot import them — not even if they find your repo on the internet. This is how you keep implementation details private: ```go // internal/config/config.go package config import "os" type Config struct { Port string DatabaseURL string LogLevel string } // Load reads config from environment variables. // This is internal — callers outside this module can't import it. func Load() Config { port := os.Getenv("PORT") if port == "" { port = "8080" } return Config{ Port: port, DatabaseURL: os.Getenv("DATABASE_URL"), LogLevel: os.Getenv("LOG_LEVEL"), } } ``` ```go // internal/store/store.go package store import ( "context" "database/sql" "fmt" _ "github.com/lib/pq" ) // Store handles database operations. type Store struct { db *sql.DB } func Connect(dsn string) (*Store, error) { db, err := sql.Open("postgres", dsn) if err != nil { return nil, fmt.Errorf("open db: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping db: %w", err) } return &Store{db: db}, nil } // GetUser fetches a user by ID. func (s *Store) GetUser(ctx context.Context, id string) (string, error) { var name string err := s.db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name) if err != nil { return "", fmt.Errorf("get user %s: %w", id, err) } return name, nil } ``` ### pkg/ — reusable public code pkg/ contains packages that are safe to import by external projects. Put things here that are genuinely reusable across projects — validation logic, utility functions, shared types. If in doubt, use internal/ instead. ```go // pkg/validate/validate.go package validate import ( "errors" "strings" ) // Email returns an error if the given string is not a valid email address. // Simple check — not RFC 5322 compliant, but good enough for most cases. func Email(email string) error { if email == "" { return errors.New("email is required") } if !strings.Contains(email, "@") || !strings.Contains(email, ".") { return errors.New("email is invalid") } return nil } // Required returns an error if the string is empty or whitespace only. func Required(field, value string) error { if strings.TrimSpace(value) == "" { return fmt.Errorf("%s is required", field) } return nil } ``` --- ## A practical example: a CLI tool GitHub Repository: https://github.com/FerRiosCosta/weather-cli Let's make this concrete with a small but complete project — a CLI that fetches weather for a city. It shows package structure, separation of concerns, and how packages talk to each other. Let's create the following directory structure: ```plaintext weather-cli/ ├── go.mod ├── cmd/ │ └── weather/ │ └── main.go └── internal/ ├── api/ │ └── api.go ← HTTP client for weather API └── display/ └── display.go ← formats output for the terminal ``` Open your terminal and create the following directories and files: ```shell mkdir weather-cli cd weather-cli mkdir -p cmd/weather touch cmd/weather/main.go mkdir -p internal/api touch internal/api/api.go mkdir -p internal/display touch internal/display/display.go ``` Open VSCode while you are in the weather-cli root directory: ```shell code . ``` Go to the Openweathermap portal and create an account if you don't have one. Once you have created it, your API key will be enabled after a couple of hours. Open a new terminal from VScode since we are going to run our program from here, but first we need to export our API KEY. ```shell export WEATHER_API_KEY=<your-api-key> ``` Now, initialize your module (or project) by running the following command: ```shell go mod init ``` You should see your new go.mod file created now: ```conf module github.com/FerRiosCosta/weather-cli go 1.26.3 ``` Start filling the files you created with the code below: ```go // internal/api/api.go package api import ( "context" "encoding/json" "fmt" "net/http" ) // WeatherResponse holds the data we care about from the API. type WeatherResponse struct { City string `json:"name"` Temperature float64 `json:"main.temp"` Description string `json:"weather.0.description"` } // apiResponse mirrors the actual nested JSON structure from OpenWeatherMap. // Go's JSON decoder doesn't support dot notation like `json:"main.temp"` — // nested fields require nested structs. // // The API returns something like: // // { // "name": "Asuncion", // "main": { "temp": 28.4 }, // "weather": [{ "description": "partly cloudy" }] // } type apiResponse struct { Name string `json:"name"` Main struct { Temp float64 `json:"temp"` } `json:"main"` Weather []struct { Description string `json:"description"` } `json:"weather"` } // Client makes requests to the weather API. type Client struct { apiKey string httpClient *http.Client } // NewClient creates and returns a new Client configured with the given API key. // This is a constructor function — Go doesn't have classes or `new` keywords, // so by convention we define a function named New<TypeName> to build a struct. func NewClient(apiKey string) *Client { return &Client{ // Store the API key so every request this client makes can use it. // It's set once here and reused — no need to pass it on every call. apiKey: apiKey, // Create a default HTTP client for making requests. // We initialize it here rather than inside GetWeather so it's // reused across calls — http.Client maintains a connection pool // internally, so reusing it is more efficient than creating a new // one each time. httpClient: &http.Client{}, } } // GetWeather fetches current weather for the given city. func (c *Client) GetWeather(ctx context.Context, city string) (*WeatherResponse, error) { url := fmt.Sprintf( "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric", city, c.apiKey, ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("fetch weather: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) } // Decode into the nested API struct first. var raw apiResponse if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { return nil, fmt.Errorf("decode response: %w", err) } // Guard against an empty weather array — the API should always return // at least one entry, but defensive code is good code. description := "" if len(raw.Weather) > 0 { description = raw.Weather[0].Description } // Return the clean, flat struct callers actually care about. return &WeatherResponse{ City: raw.Name, Temperature: raw.Main.Temp, Description: description, }, nil } ``` ```go // internal/display/display.go package display import ( "fmt" "io" "github.com/yourname/weather-cli/internal/api" ) // Weather prints a formatted weather summary to w. func Weather(w io.Writer, data *api.WeatherResponse) { fmt.Fprintf(w, "City: %s\n", data.City) fmt.Fprintf(w, "Temperature: %.1f°C\n", data.Temperature) fmt.Fprintf(w, "Conditions: %s\n", data.Description) } ``` ```go // cmd/weather/main.go package main import ( "context" "log/slog" "os" "github.com/yourname/weather-cli/internal/api" "github.com/yourname/weather-cli/internal/display" ) func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "usage: weather <city>") os.Exit(1) } city := os.Args[1] apiKey := os.Getenv("WEATHER_API_KEY") if apiKey == "" { slog.Error("WEATHER_API_KEY environment variable is required") os.Exit(1) } client := api.NewClient(apiKey) weather, err := client.GetWeather(context.Background(), city) if err != nil { slog.Error("failed to get weather", "city", city, "error", err) os.Exit(1) } display.Weather(os.Stdout, weather) } ``` Run it: ```console go run ./cmd/weather Asuncion City: Asuncion Temperature: 28.4°C Conditions: partly cloudy ``` Notice how each package has one job: - api — knows how to talk to the weather API. Nothing else. - display — knows how to format output. Nothing else. - cmd/weather/main.go — wires them together. Nothing else. Adding a second output format (JSON, CSV) means a new file in display/. Adding a second weather provider means a new file in api/. Neither change touches the other. --- ## Common gotchas ### 1. Circular imports — Go forbids them Package A cannot import package B if package B imports package A. Go will refuse to compile. ```go // This will not compile package a imports package b package b imports package a ← circular — error ``` The fix: extract the shared type into a third package that neither A nor B imports from each other. ### 2. Package name vs directory name The directory name and the package name don't have to match — but they should. The one common exception is main: the directory is usually named after the binary (server, worker, weather), but the package is always package main. ```go // Directory: cmd/server/ // File: main.go package main // always "main", not "server" ``` ### 3. Trying to have multiple packages in one directory Every .go file in a directory must declare the same package name (with the exception of _test.go files, which can use package foo_test for black-box testing). This is a compile error: ```plaintext myapp/ ├── server.go ← package server └── handler.go ← package handler ← ERROR: can't mix packages in one directory ``` ### 4. Exporting by accident If you capitalize a function name, it's exported — immediately accessible to anyone who imports the package. This isn't always what you want, especially for internal helpers. Default to lowercase until you know something needs to be public. --- ## Summary Go's package and module system is built on a few simple rules that compound into something powerful. Four things to take away: 1. A package is a folder — everything in it shares a namespace, capital letters are exported. 2. A module is a versioned collection of packages — defined by go.mod, locked by go.sum. 3. Use cmd/ for entry points, internal/ for private code, pkg/ for reusable public code. 4. go mod tidy is your best friend — run it whenever you add, remove, or change dependencies. The project structure in this post is the same one I'll use for every project on this blog. Every Go + Lambda, Go + Kubernetes, and Go + AWS post builds on it. Once it's familiar, you'll recognize it instantly in any Go project you open on GitHub.

    Tags

    godevops

    Comments

    More Blog

    View all
    Minimalist EKS: The Easy Waykubernetes

    Minimalist EKS: The Easy Way

    Amazon EKS manages the Kubernetes control plane, but you remain responsible for provisioning the...

    J
    Joaquin Menchaca
    Never forget to enter the Stern Grove lottery again!ai

    Never forget to enter the Stern Grove lottery again!

    Browser automation with Playwright, Python, GitHub Actions, and Entire to auto-enter San Francisco Stern Grove concert lotteries each week!

    L
    Lizzie Siegle
    A Free Screenshot Editor That Never Uploads Your Imagetypescript

    A Free Screenshot Editor That Never Uploads Your Image

    A free screenshot and image editor that runs entirely in your browser. Keeping every edit reversible and handling big phone photos, in plain TypeScript and Canvas2D.

    M
    Martin Stark
    I built a CLI to break my highlights out of Apple Booksshowdev

    I built a CLI to break my highlights out of Apple Books

    A macOS CLI + MCP server that exports Apple Books highlights to Markdown and gives AI assistants direct access to your reading notes.

    A
    Andrey Korchak
    A Developer's Guide to Agent Hooks in Antigravity CLIai

    A Developer's Guide to Agent Hooks in Antigravity CLI

    Motivation To be quite honest, "Hooks"—the shell commands we trigger at specific points...

    T
    Tanaike
    Tactical vs. Strategic Agentic AI Development — A Playbook for Developersagents

    Tactical vs. Strategic Agentic AI Development — A Playbook for Developers

    The Strategic Engineer: Why Writing Code Is No Longer Your Most Valuable Skill ...

    A
    Adewumi Saheed Adewale

    Stay up to date

    Get the latest CoPilot prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for CoPilot and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.