Go — Overview
Go is an open-source programming language designed at Google for building simple, efficient, and reliable software at scale. First released in 2009 and reaching maturity with Go 1 in 2012, Go combines the performance and safety of a statically typed compiled language with the readability and productivity of a dynamically typed one.
Go's design prioritizes clarity over cleverness. It has a minimal syntax, a fast compiler, garbage collection, built-in concurrency primitives called goroutines, and a powerful standard library. These qualities make it a popular choice for cloud-native services, command-line tools, network servers, DevOps tooling, and distributed systems.
info
The official Go toolchain is available for macOS, Windows, Linux, and BSD. Download the installer from go.dev/dl or use a package manager. After installation, verify that the go command is available and the GOPATH and GOROOT environment variables are set correctly.
| 1 | # macOS with Homebrew |
| 2 | brew install go |
| 3 | |
| 4 | # Windows with winget |
| 5 | winget install GoLang.Go |
| 6 | |
| 7 | # Ubuntu/Debian |
| 8 | sudo apt update && sudo apt install golang-go |
| 9 | |
| 10 | # Verify installation |
| 11 | go version |
| 12 | # go version go1.24.0 darwin/arm64 |
| 13 | |
| 14 | # Inspect environment |
| 15 | go env GOPATH GOROOT GO111MODULE |
Modern Go development uses modules for dependency management. A module is a collection of Go packages stored in a file tree with a go.mod file at its root. Initialize a new module with go mod init.
| 1 | mkdir hello && cd hello |
| 2 | go mod init example.com/hello |
| 3 | # creates go.mod: module example.com/hello |
| 4 | |
| 5 | cat go.mod |
| 6 | # module example.com/hello |
| 7 | # |
| 8 | # go 1.24 |
note
Every Go executable starts in the main package and calls the main() function. Create a file named main.go, write a small program, and run it with go run or build a binary with go build.
| 1 | package main |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | func main() { |
| 6 | fmt.Println("Hello, ForgeLearn!") |
| 7 | } |
| 1 | # Run directly |
| 2 | go run main.go |
| 3 | # Hello, ForgeLearn! |
| 4 | |
| 5 | # Build a native binary |
| 6 | go build -o hello main.go |
| 7 | ./hello |
| 8 | # Hello, ForgeLearn! |
| 9 | |
| 10 | # Cross-compile for Linux on macOS |
| 11 | GOOS=linux GOARCH=amd64 go build -o hello-linux main.go |
best practice
Go is statically typed with a C-like syntax but without semicolons, parentheses around conditions, or header files. The language is intentionally small: there are no classes, generics until Go 1.18, inheritance, or exceptions. Instead, Go provides packages, structs, interfaces, and explicit error returns.
| Concept | Go Syntax | Notes |
|---|---|---|
| Variables | var name string = "Go" | Explicit declaration with var, or short declaration with := |
| Constants | const Pi = 3.14159 | Cannot be changed after declaration |
| Types | int, float64, string, bool, []T, map[K]V | Zero values are initialized automatically |
| Functions | func add(a, b int) int {} | Multiple return values supported |
| Control | if, for, switch, defer | No while loop; for handles all iteration |
| Structs | type User struct { Name string } | Composite types with named fields |
| Interfaces | type Reader interface { Read(p []byte) (n int, err error) } | Satisfied implicitly by implementing methods |
| 1 | package main |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | func main() { |
| 6 | // Explicit declaration |
| 7 | var language string = "Go" |
| 8 | |
| 9 | // Short declaration (type inferred) |
| 10 | version := 1.24 |
| 11 | |
| 12 | // Multiple variables |
| 13 | var ( |
| 14 | author = "Google" |
| 15 | release = 2009 |
| 16 | ) |
| 17 | |
| 18 | // Constants |
| 19 | const maxRetries = 3 |
| 20 | |
| 21 | fmt.Printf("%s %.2f by %s, released %d\n", language, version, author, release) |
| 22 | } |
warning
Go uses if, for, and switch for control flow. There is no while or do-while; the for loop subsumes all iteration patterns. Statements can include an optional initialization clause.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | ) |
| 7 | |
| 8 | func main() { |
| 9 | // if with initialization |
| 10 | if file, err := os.Open("data.txt"); err == nil { |
| 11 | defer file.Close() |
| 12 | fmt.Println("opened successfully") |
| 13 | } else { |
| 14 | fmt.Println("error:", err) |
| 15 | } |
| 16 | |
| 17 | // Classic for loop |
| 18 | for i := 0; i {'<'} 3; i++ { |
| 19 | fmt.Println(i) |
| 20 | } |
| 21 | |
| 22 | // For as while |
| 23 | n := 0 |
| 24 | for n {'<'} 3 { |
| 25 | fmt.Println(n) |
| 26 | n++ |
| 27 | } |
| 28 | |
| 29 | // Infinite loop with break |
| 30 | for { |
| 31 | if n {'>'} 5 { |
| 32 | break |
| 33 | } |
| 34 | n++ |
| 35 | } |
| 36 | |
| 37 | // Range over slice |
| 38 | names := []string{"Alice", "Bob", "Carol"} |
| 39 | for index, name := range names { |
| 40 | fmt.Printf("%d: %s\n", index, name) |
| 41 | } |
| 42 | |
| 43 | // Switch |
| 44 | role := "admin" |
| 45 | switch role { |
| 46 | case "admin": |
| 47 | fmt.Println("full access") |
| 48 | case "editor": |
| 49 | fmt.Println("edit access") |
| 50 | default: |
| 51 | fmt.Println("read only") |
| 52 | } |
| 53 | } |
best practice
Functions are first-class values in Go. They can accept multiple parameters, return multiple values, and be passed around as variables. Named return values and variadic parameters are supported, making functions flexible and expressive.
| 1 | package main |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | // Multiple parameters of the same type |
| 6 | func rectangle(width, height int) int { |
| 7 | return width * height |
| 8 | } |
| 9 | |
| 10 | // Multiple return values |
| 11 | func divide(dividend, divisor float64) (float64, error) { |
| 12 | if divisor == 0 { |
| 13 | return 0, fmt.Errorf("cannot divide by zero") |
| 14 | } |
| 15 | return dividend / divisor, nil |
| 16 | } |
| 17 | |
| 18 | // Named return values |
| 19 | func split(sum int) (x, y int) { |
| 20 | x = sum * 4 / 9 |
| 21 | y = sum - x |
| 22 | return // naked return uses named values |
| 23 | } |
| 24 | |
| 25 | // Variadic function |
| 26 | func sum(numbers ...int) int { |
| 27 | total := 0 |
| 28 | for _, n := range numbers { |
| 29 | total += n |
| 30 | } |
| 31 | return total |
| 32 | } |
| 33 | |
| 34 | func main() { |
| 35 | fmt.Println(rectangle(4, 5)) |
| 36 | |
| 37 | result, err := divide(10, 2) |
| 38 | if err != nil { |
| 39 | fmt.Println("error:", err) |
| 40 | } else { |
| 41 | fmt.Println(result) |
| 42 | } |
| 43 | |
| 44 | fmt.Println(split(17)) |
| 45 | fmt.Println(sum(1, 2, 3, 4, 5)) |
| 46 | } |
Multiple return values are the foundation of Go's error-handling model. By convention, the last return value is an error, and callers check it before using other results. This makes error paths explicit and local.
Go programs are organized into packages. A package is a directory of .go files that share the same package declaration. Packages provide namespacing, encapsulation through capitalization, and reusable APIs. Modules manage dependencies between packages and versions.
| 1 | // mathutils/mathutils.go |
| 2 | package mathutils |
| 3 | |
| 4 | // Add is exported because it starts with a capital letter. |
| 5 | func Add(a, b int) int { |
| 6 | return a + b |
| 7 | } |
| 8 | |
| 9 | // multiply is unexported and only visible within the package. |
| 10 | func multiply(a, b int) int { |
| 11 | return a * b |
| 12 | } |
| 1 | // main.go |
| 2 | package main |
| 3 | |
| 4 | import ( |
| 5 | "fmt" |
| 6 | |
| 7 | "example.com/hello/mathutils" |
| 8 | ) |
| 9 | |
| 10 | func main() { |
| 11 | result := mathutils.Add(2, 3) |
| 12 | fmt.Println(result) // 5 |
| 13 | } |
Visibility in Go is simple: identifiers starting with an uppercase letter are exported and accessible from other packages; lowercase identifiers are package-private. There are no public, private, or protected keywords.
| 1 | # Add an external dependency |
| 2 | go get github.com/gin-gonic/gin |
| 3 | |
| 4 | # Update all dependencies |
| 5 | go get -u ./... |
| 6 | |
| 7 | # Tidy go.mod and download missing modules |
| 8 | go mod tidy |
| 9 | |
| 10 | # List available module upgrades |
| 11 | go list -m -u all |
| 12 | |
| 13 | # Vendor dependencies into the project |
| 14 | go mod vendor |
info
Structs are Go's way of grouping related data. A method is a function with a special receiver argument that binds it to a type. Methods turn structs into lightweight objects without inheritance. Composition, achieved by embedding structs, is preferred over subclassing.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | type User struct { |
| 9 | ID int |
| 10 | Email string |
| 11 | CreatedAt time.Time |
| 12 | } |
| 13 | |
| 14 | // Value receiver: does not modify the original. |
| 15 | func (u User) DisplayName() string { |
| 16 | return u.Email |
| 17 | } |
| 18 | |
| 19 | // Pointer receiver: can modify the original. |
| 20 | func (u *User) Verify() { |
| 21 | u.ID = -u.ID |
| 22 | } |
| 23 | |
| 24 | type Admin struct { |
| 25 | User // embedded struct |
| 26 | Role string |
| 27 | } |
| 28 | |
| 29 | func main() { |
| 30 | u := User{ |
| 31 | ID: 1, |
| 32 | Email: "alice@example.com", |
| 33 | CreatedAt: time.Now(), |
| 34 | } |
| 35 | |
| 36 | fmt.Println(u.DisplayName()) |
| 37 | u.Verify() |
| 38 | fmt.Println(u.ID) |
| 39 | |
| 40 | a := Admin{ |
| 41 | User: u, |
| 42 | Role: "superuser", |
| 43 | } |
| 44 | fmt.Println(a.Email) // promoted field from embedded User |
| 45 | } |
best practice
Interfaces in Go are satisfied implicitly. A type implements an interface simply by implementing its methods; there is no implements keyword. This design encourages small, focused interfaces and loose coupling between packages.
| 1 | package main |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | // Small, focused interface — the Go way. |
| 6 | type Greeter interface { |
| 7 | Greet() string |
| 8 | } |
| 9 | |
| 10 | type Person struct { |
| 11 | Name string |
| 12 | } |
| 13 | |
| 14 | func (p Person) Greet() string { |
| 15 | return "Hello, " + p.Name |
| 16 | } |
| 17 | |
| 18 | type Robot struct { |
| 19 | Model string |
| 20 | } |
| 21 | |
| 22 | func (r Robot) Greet() string { |
| 23 | return "Beep boop, " + r.Model |
| 24 | } |
| 25 | |
| 26 | func sayHello(g Greeter) { |
| 27 | fmt.Println(g.Greet()) |
| 28 | } |
| 29 | |
| 30 | func main() { |
| 31 | sayHello(Person{Name: "Alice"}) |
| 32 | sayHello(Robot{Model: "R2-D2"}) |
| 33 | } |
The empty interface interface{} matches every type. It is useful for generic containers or interoperating with unknown data, but prefer concrete types or type parameters when possible to preserve compile-time safety.
info
Go does not use exceptions for routine error handling. Instead, functions return an error value as the last result. Callers check the error immediately and decide whether to handle it, wrap it, or return it up the stack. This makes every error path visible in the source code.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | ) |
| 7 | |
| 8 | var ErrNotFound = errors.New("resource not found") |
| 9 | |
| 10 | func fetchUser(id int) (string, error) { |
| 11 | if id {'<'}= 0 { |
| 12 | return "", fmt.Errorf("invalid user id: %d", id) |
| 13 | } |
| 14 | if id == 42 { |
| 15 | return "", ErrNotFound |
| 16 | } |
| 17 | return "Alice", nil |
| 18 | } |
| 19 | |
| 20 | func main() { |
| 21 | name, err := fetchUser(42) |
| 22 | if err != nil { |
| 23 | if errors.Is(err, ErrNotFound) { |
| 24 | fmt.Println("not found") |
| 25 | } else { |
| 26 | fmt.Println("error:", err) |
| 27 | } |
| 28 | return |
| 29 | } |
| 30 | fmt.Println(name) |
| 31 | } |
For production code, wrap errors with context using fmt.Errorf("...: %w", err). The %w verb creates a wrapped error that can be inspected with errors.Is and errors.As.
| 1 | type ValidationError struct { |
| 2 | Field string |
| 3 | } |
| 4 | |
| 5 | func (e *ValidationError) Error() string { |
| 6 | return fmt.Sprintf("validation failed for field %q", e.Field) |
| 7 | } |
| 8 | |
| 9 | func saveUser(name string) error { |
| 10 | if name == "" { |
| 11 | return &ValidationError{Field: "name"} |
| 12 | } |
| 13 | return nil |
| 14 | } |
| 15 | |
| 16 | func main() { |
| 17 | err := saveUser("") |
| 18 | var validationErr *ValidationError |
| 19 | if errors.As(err, &validationErr) { |
| 20 | fmt.Println(validationErr.Field) |
| 21 | } |
| 22 | } |
danger
Concurrency is a first-class feature of Go. Goroutines are lightweight threads managed by the Go runtime; channels are typed conduits for communication between goroutines. The mantra is: do not communicate by sharing memory; instead, share memory by communicating.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | func worker(id int, jobs {'<'}-chan int, results chan{'<-'} int) { |
| 9 | for job := range jobs { |
| 10 | fmt.Printf("worker %d processing job %d\n", id, job) |
| 11 | time.Sleep(time.Millisecond * 100) |
| 12 | results {'<'} - job * 2 |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | func main() { |
| 17 | jobs := make(chan int, 10) |
| 18 | results := make(chan int, 10) |
| 19 | |
| 20 | for w := 1; w {'<'}= 3; w++ { |
| 21 | go worker(w, jobs, results) |
| 22 | } |
| 23 | |
| 24 | for j := 1; j {'<'}= 9; j++ { |
| 25 | jobs {'<'} - j |
| 26 | } |
| 27 | close(jobs) |
| 28 | |
| 29 | for a := 1; a {'<'}= 9; a++ { |
| 30 | {'<'} -results |
| 31 | } |
| 32 | } |
Use buffered channels when you know the number of messages, and unbuffered channels for synchronization. The sync package provides WaitGroups, Mutexes, and Once for coordination when channels alone would be awkward.
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "sync" |
| 6 | ) |
| 7 | |
| 8 | func main() { |
| 9 | var wg sync.WaitGroup |
| 10 | var mu sync.Mutex |
| 11 | counter := 0 |
| 12 | |
| 13 | for i := 0; i {'<'} 100; i++ { |
| 14 | wg.Add(1) |
| 15 | go func() { |
| 16 | defer wg.Done() |
| 17 | mu.Lock() |
| 18 | counter++ |
| 19 | mu.Unlock() |
| 20 | }() |
| 21 | } |
| 22 | |
| 23 | wg.Wait() |
| 24 | fmt.Println(counter) |
| 25 | } |
warning
Go ships with a comprehensive toolchain. Formatting, testing, benchmarking, profiling, documentation generation, and dependency management are all included. Mastering these tools is as important as learning the language itself.
| Command | Purpose |
|---|---|
| go build | Compile packages and dependencies |
| go run | Compile and run a program |
| go test | Run tests and benchmarks |
| go fmt | Format source files |
| go vet | Static analysis for likely mistakes |
| go mod | Dependency and module management |
| go doc | Show documentation for packages and symbols |
| go generate | Run code generators via //go:generate directives |
| 1 | # Format all files in the current module |
| 2 | go fmt ./... |
| 3 | |
| 4 | # Run tests with coverage |
| 5 | go test -cover ./... |
| 6 | |
| 7 | # Run benchmarks |
| 8 | go test -bench=. -benchmem ./... |
| 9 | |
| 10 | # Static analysis |
| 11 | go vet ./... |
| 12 | |
| 13 | # Build with race detector |
| 14 | go run -race main.go |
| 15 | |
| 16 | # Profile a program |
| 17 | go test -cpuprofile=cpu.out ./... |
| 18 | go tool pprof cpu.out |
Go's testing package is minimal but effective. Test files end with _test.go and contain functions starting with Test, Benchmark, or Example. Table-driven tests are the idiomatic way to test multiple cases.
| 1 | package mathutils |
| 2 | |
| 3 | import "testing" |
| 4 | |
| 5 | func TestAdd(t *testing.T) { |
| 6 | cases := []struct { |
| 7 | a, b, want int |
| 8 | }{ |
| 9 | {1, 2, 3}, |
| 10 | {0, 0, 0}, |
| 11 | {-1, 1, 0}, |
| 12 | } |
| 13 | |
| 14 | for _, c := range cases { |
| 15 | got := Add(c.a, c.b) |
| 16 | if got != c.want { |
| 17 | t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want) |
| 18 | } |
| 19 | } |
| 20 | } |
info
Go has a mature ecosystem of libraries and frameworks for web services, cloud infrastructure, databases, and developer tooling. Many foundational cloud-native projects — including Docker, Kubernetes, Terraform, and Prometheus — are written in Go.
| Category | Popular Choices |
|---|---|
| Web frameworks | Gin, Echo, Chi, Fiber, standard library net/http |
| Databases | database/sql, GORM, sqlx, pgx, mongo-driver, Redis clients |
| Configuration | Viper, envconfig, caarlos0/env |
| CLI | cobra, urfave/cli, spf13/viper |
| Observability | OpenTelemetry, Zap, slog, Prometheus client |
| Testing | testify, gomock, httpexpect, fuzzing in Go 1.18+ |
| Cloud SDKs | AWS SDK for Go v2, Google Cloud Go, Azure SDK |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | |
| 6 | "github.com/gin-gonic/gin" |
| 7 | ) |
| 8 | |
| 9 | func main() { |
| 10 | r := gin.Default() |
| 11 | r.GET("/health", func(c *gin.Context) { |
| 12 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) |
| 13 | }) |
| 14 | r.Run(":8080") |
| 15 | } |
note
Writing idiomatic Go means embracing simplicity and explicitness. The following practices will keep your code clean, testable, and maintainable as your project grows.
| Do | Don't |
|---|---|
| Handle errors explicitly at every layer | Ignore errors with _ or panic for routine failures |
| Keep interfaces small and focused | Create large interfaces with many methods |
| Use composition to share behavior | Simulate inheritance with deep type hierarchies |
| Write table-driven tests | Copy-paste nearly identical test cases |
| Document exported symbols with comments | Leave public APIs undocumented |
| Use context for cancellation and deadlines | Rely on goroutines without lifecycle management |
| Run gofmt, go vet, and go test in CI | Merge code without static analysis or tests |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | func fetchWithTimeout(ctx context.Context) error { |
| 10 | ctx, cancel := context.WithTimeout(ctx, 2*time.Second) |
| 11 | defer cancel() |
| 12 | |
| 13 | select { |
| 14 | case {'<'} -ctx.Done(): |
| 15 | return ctx.Err() |
| 16 | case {'<'} -time.After(100 * time.Millisecond): |
| 17 | return nil |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | func main() { |
| 22 | if err := fetchWithTimeout(context.Background()); err != nil { |
| 23 | fmt.Println("failed:", err) |
| 24 | } |
| 25 | } |
best practice
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.