How Go projects grow from one file to many. Packages, visibility, and organization.
This is the first thing that confuses people from Python. In Go, all files in the same directory with the same package declaration are ONE unit. They share everything automatically.
package main
func main() {
cfg := LoadConfig() // Defined in config.go
Process(cfg) // Defined in process.go
}
package main
type Config struct {
Name string
}
func LoadConfig() *Config {
return &Config{Name: "test"}
}
package main
func Process(cfg *Config) { // Can use Config from config.go
fmt.Println(cfg.Name)
}
No imports between them. They just... work together. The compiler sees them as one blob.
# Compiles ALL .go files in current directory
$ go run .
# Or explicitly
$ go run main.go config.go process.go
There's no hard rule, but here's what works:
Don't over-split. A 200-line file is fine. Ten 20-line files is annoying.
When your project gets bigger, you'll want separate packages. Each directory is a package.
myproject/
├── go.mod # module github.com/you/myproject
├── main.go # package main
└── stuff/
└── stuff.go # package stuff
package stuff
func DoThing() string {
return "did the thing"
}
package main
import "github.com/you/myproject/stuff"
func main() {
result := stuff.DoThing()
fmt.Println(result)
}
Import Path: The import path is your module name (from go.mod) + the directory path. Not the file path. Not the package name. The directory.
This is Go's visibility system. Dead simple once you get it.
package stuff
// Exported — visible outside this package
func PublicFunc() {} // ✓ Uppercase first letter
type PublicType struct{} // ✓
var PublicVar = 42 // ✓
// Unexported — only visible inside this package
func privateFunc() {} // ✗ Lowercase first letter
type privateType struct{} // ✗
var privateVar = 42 // ✗
From another package, you can only access the uppercase stuff:
import "github.com/you/myproject/stuff"
stuff.PublicFunc() // ✓ Works
stuff.privateFunc() // ✗ Compile error
Go has one magic directory name: internal/
Code inside internal/ can only be imported by code in the parent directory tree. The compiler enforces this.
myproject/
├── go.mod
├── main.go # Can import internal/secret
├── cmd/
│ └── tool/
│ └── main.go # Can import internal/secret
└── internal/
└── secret/
└── secret.go # package secret
If someone else imports your module, they cannot import anything from your internal/ directory. Compiler stops them.
When to use internal/: Put implementation details you don't want to be part of your public API. You can refactor internal code freely without breaking anyone.
mytool/
├── go.mod
├── main.go # Everything in one package
├── config.go
└── commands.go
mytool/
├── go.mod
├── main.go
├── cmd/ # CLI command definitions
│ ├── root.go
│ └── serve.go
└── internal/ # Private implementation
├── config/
└── server/
mylib/
├── go.mod
├── mylib.go # Public library API (package mylib)
├── cmd/
│ └── mylib/
│ └── main.go # CLI that uses the library
└── internal/ # Shared private code
Go doesn't allow circular imports. If package A imports B, B cannot import A.
// a/a.go
package a
import "myproject/b" // A imports B
// b/b.go
package b
import "myproject/a" // B imports A — ERROR!
Solutions:
Progress through each section in order, or jump to where you need practice.
Practice individual concepts you just learned.
Combine concepts and learn patterns. Each challenge has multiple variants at different difficulties.