惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
Recorded Future
Recorded Future
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
GbyAI
GbyAI
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
M
MIT News - Artificial intelligence
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
腾讯CDC
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
aimingoo的专栏
aimingoo的专栏
博客园 - Franky
Martin Fowler
Martin Fowler
P
Proofpoint News Feed
月光博客
月光博客
博客园_首页
C
Check Point Blog
宝玉的分享
宝玉的分享
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Tenable Blog
Vercel News
Vercel News
SecWiki News
SecWiki News
Security Latest
Security Latest
Microsoft Security Blog
Microsoft Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
阮一峰的网络日志
阮一峰的网络日志
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
Y
Y Combinator Blog
U
Unit 42
The Register - Security
The Register - Security
云风的 BLOG
云风的 BLOG
K
Kaspersky official blog
WordPress大学
WordPress大学
Blog — PlanetScale
Blog — PlanetScale
N
Netflix TechBlog - Medium
Jina AI
Jina AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Go Packages and Modules explained
Fer Rios · 2026-06-11 · via DEV Community

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.

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:

// 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
}

// math/sub.go
package math

// Exported — starts with a capital letter.
func Subtract(a, b int) int {

    return a-b
}

// 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:

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:

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).

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

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:

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

# 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:

go mod tidy

go mod tidy will remove the require entry automatically if nothing in your code imports it anymore.

Viewing your dependency tree

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:

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:

go get github.com/spf13/cobra@v1.8.0

Your go.mod now contains:

require (
    github.com/spf13/cobra v1.8.0
)

Cobra is a module at github.com/spf13/cobra. Inside that module, there are multiple packages:

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.

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.

// This is a runnable program
package main

import "fmt"

func main() {
    fmt.Println("hello from main")
}

// 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.

module github.com/yourname/myapp   ← module path

myapp/
├── go.mod
└── internal/
    └── config/
        └── config.go              ← import path: github.com/yourname/myapp/internal/config

// 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:

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":

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:

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.

// 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:

// 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"),
    }
}

// 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.

// 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:

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:

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:

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.

export WEATHER_API_KEY=<your-api-key>

Now, initialize your module (or project) by running the following command:

go mod init

You should see your new go.mod file created now:

module github.com/FerRiosCosta/weather-cli

go 1.26.3

Start filling the files you created with the code below:

// 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
}

// 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)
}

// 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:

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.

// 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.

// 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:

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.