When you ask AI to build a Go project, it has to make structural decisions: how many packages, what goes where, what's exported. These decisions are harder to evaluate than syntax errors because bad package design compiles and runs fine, it just creates a maintenance nightmare. This lesson focuses on evaluating AI's structural choices.
The package system
Every .go file declares its package on line one. All files in the same directory must declare the same package. The package name should match the directory name:
myproject/
├── main.go // package main
├── auth/
│ ├── login.go // package auth
│ └── token.go // package auth
├── models/
│ └── user.go // package models
└── handlers/
└── api.go // package handlerspackage main is special, it's the entry point for executables. Every other package is a library that gets imported.
package main, all types, all handlers, all utilities in one giant main package. This works for tiny scripts but becomes unreadable fast. If AI generates a project with 500+ lines in package main, ask it to split into logical packages.Export rules: uppercase = public
Go's visibility rule is the simplest in any language: uppercase first letter means exported, lowercase means unexported.
package auth
type User struct { // Exported - other packages can use it
Name string // Exported field
email string // Unexported - only auth package can access
}
func Authenticate(token string) (*User, error) { // Exported
return validateToken(token)
}
func validateToken(t string) (*User, error) { // Unexported
// internal implementation
}
const MaxRetries = 3 // Exported
var defaultTimeout = 30 // UnexportedThis applies to everything: functions, types, struct fields, methods, constants, variables. No public/private keywords exist in Go.
| Identifier | Visibility | Accessible from |
|---|---|---|
UserService | Exported | Any importing package |
userService | Unexported | Same package only |
user.Name | Exported field | Any code with a User value |
user.email | Unexported field | Same package only |
package auth can access each other's unexported names. This surprises people coming from languages where private means file-private.Modules and go.mod
A moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. is a collection of packages with a go.mod file at the root. This file tracks the module path and all dependencies:
module github.com/yourname/myproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)The module path is your project's unique identifier. It's typically a URL where the code lives, though it doesn't have to be hosted there.
Essential commands
| Command | What it does | When to use |
|---|---|---|
go mod init github.com/you/project | Creates go.mod | Starting a new project |
go get github.com/pkg/errors | Adds a dependency | Adding a library |
go get -u ./... | Updates all deps | Updating everything |
go mod tidy | Removes unused, adds missing | Before every commit |
go mod download | Downloads all deps | CI/CD, fresh clones |
go mod tidy is the one you'll run most. It ensures go.mod and go.sum match what your code actually imports.
The init() function
Every package can have an init() function that runs automatically when the package is imported, before main():
package config
var DatabaseURL string
func init() {
DatabaseURL = os.Getenv("DATABASE_URL")
if DatabaseURL == "" {
DatabaseURL = "postgres://localhost/mydb"
}
}init() runs once, automatically, with no explicit call. A package can have multiple init() functions across files.
init() because it's convenient. But init() functions are implicit, they run without being called, making code harder to trace and test. Prefer explicit initialization:> // AI generates this (implicit, hard to test):
> func init() {
> db = connectDB()
> }
>
> // Better (explicit, testable):
> func SetupDB(connStr string) (*DB, error) {
> return sql.Open("postgres", connStr)
> }
>Use
init() only for truly simple, side-effect-free setup like registering drivers.Blank imports
Sometimes you import a package purely for its side effects (its init() function), without using any exported names:
import (
"database/sql"
_ "github.com/lib/pq" // registers PostgreSQL driver via init()
)The _ tells Go "I know I'm not using names from this package." Common for database drivers and image format decoders.
| Import style | Syntax | Purpose |
|---|---|---|
| Regular | import "fmt" | Use exported names |
| Aliased | import f "fmt" | Rename to avoid conflicts |
| Blank | import _ "driver" | Side effects only |
| Dot | import . "fmt" | Import names into current scope (avoid) |
Evaluating AI's package structure
When AI scaffolds a Go project, evaluate the structure against these principles:
Good signs
- Each package has a single clear purpose (
auth,storage,handlers) - Minimal exported APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., only what callers need
- No circular dependencies
internal/directory for code that shouldn't be imported externally
Red flags
- Everything in
package main - Packages named
utils,helpers,common(these become junk drawers) - Circular imports (won't compile, but AI generates them)
- Every field and function exported
| Package name | Verdict | Why |
|---|---|---|
auth | Good | Clear purpose, descriptive |
handlers | Good | Standard Go convention |
utils | Suspicious | What utility? Too vague |
helpers | Bad | Meaningless, what does it help with? |
common | Bad | Everything "common" belongs somewhere specific |
models | Fine | Widely understood convention |
A well-structured project
Here's what a solid Go project structure looks like. Use this as a reference when evaluating AI output:
bookstore/
├── go.mod
├── main.go // package main - wiring only
├── internal/ // can't be imported by external projects
│ ├── auth/
│ │ └── auth.go // package auth
│ └── storage/
│ └── memory.go // package storage
├── models/
│ └── book.go // package models - shared types
└── handlers/
└── book.go // package handlers// models/book.go
package models
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Price float64 `json:"price"`
}
// handlers/book.go
package handlers
import "bookstore/models"
type BookStore interface {
Get(id string) (*models.Book, error)
Save(book *models.Book) error
}
type BookHandler struct {
store BookStore // depends on interface, not concrete type
}Notice: BookHandler depends on an interface, not a concrete storage type. This means you can swap implementations (memory, PostgreSQL, mockWhat is mock?A fake replacement for a real dependency in tests that records how it was called so you can verify interactions. for tests) without changing the handler code. When AI generates handler code with direct database dependencies, suggest introducing an interface.