Go/
Lesson

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 handlers

package main is special, it's the entry point for executables. Every other package is a library that gets imported.

AI pitfall
AI sometimes puts everything in 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.
02

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

This applies to everything: functions, types, struct fields, methods, constants, variables. No public/private keywords exist in Go.

IdentifierVisibilityAccessible from
UserServiceExportedAny importing package
userServiceUnexportedSame package only
user.NameExported fieldAny code with a User value
user.emailUnexported fieldSame package only
AI pitfall
AI frequently exports everything, every function, every struct field, every constant. This creates a public API surface that's hard to maintain and signals that the AI isn't thinking about encapsulation. When reviewing AI-generated packages, ask: "Does the caller actually need access to this?" If not, lowercase it. A smaller exported API is easier to understand and safer to refactor.
Visibility is package-level, not file-level. Two files in package auth can access each other's unexported names. This surprises people coming from languages where private means file-private.
03

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

CommandWhat it doesWhen to use
go mod init github.com/you/projectCreates go.modStarting a new project
go get github.com/pkg/errorsAdds a dependencyAdding a library
go get -u ./...Updates all depsUpdating everything
go mod tidyRemoves unused, adds missingBefore every commit
go mod downloadDownloads all depsCI/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.

04

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.

AI pitfall
AI loves 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.
05

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 styleSyntaxPurpose
Regularimport "fmt"Use exported names
Aliasedimport f "fmt"Rename to avoid conflicts
Blankimport _ "driver"Side effects only
Dotimport . "fmt"Import names into current scope (avoid)
06

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 nameVerdictWhy
authGoodClear purpose, descriptive
handlersGoodStandard Go convention
utilsSuspiciousWhat utility? Too vague
helpersBadMeaningless, what does it help with?
commonBadEverything "common" belongs somewhere specific
modelsFineWidely understood convention
AI pitfall
AI creates circular dependencies when it's not tracking import relationships. Package A imports B, and B imports A. This won't compile. AI then tries to fix it by merging everything into one package, which is worse. The real fix is restructuring: extract shared types into a third package, or use interfaces to break the cycle.
07

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.