|$ curl https://forge-ai.dev/api/markdown?path=docs/go
$cat docs/go-—-overview.md
updated Recently·18 min read·published

Go — Overview

GoBackendSystemsBeginner🎯Free Tools
Introduction

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

Go values readability and simplicity at the language level. There is usually one obvious way to do things, which makes Go code easy to review, maintain, and onboard new engineers into.
Installation & Setup

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.

install-go.sh
Bash
1# macOS with Homebrew
2brew install go
3
4# Windows with winget
5winget install GoLang.Go
6
7# Ubuntu/Debian
8sudo apt update && sudo apt install golang-go
9
10# Verify installation
11go version
12# go version go1.24.0 darwin/arm64
13
14# Inspect environment
15go 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.

init-module.sh
Bash
1mkdir hello && cd hello
2go mod init example.com/hello
3# creates go.mod: module example.com/hello
4
5cat go.mod
6# module example.com/hello
7#
8# go 1.24
📝

note

Since Go 1.16, module mode is enabled by default. You rarely need to think about GOPATH unless you are working with legacy codebases.
Your First Go Program

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.

main.go
GO
1package main
2
3import "fmt"
4
5func main() {
6 fmt.Println("Hello, ForgeLearn!")
7}
run-go.sh
Bash
1# Run directly
2go run main.go
3# Hello, ForgeLearn!
4
5# Build a native binary
6go build -o hello main.go
7./hello
8# Hello, ForgeLearn!
9
10# Cross-compile for Linux on macOS
11GOOS=linux GOARCH=amd64 go build -o hello-linux main.go

best practice

Use go run during development and go build for producing distributable binaries. Go compiles to a single static binary by default, which simplifies deployment.
Language Fundamentals

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.

ConceptGo SyntaxNotes
Variablesvar name string = "Go"Explicit declaration with var, or short declaration with :=
Constantsconst Pi = 3.14159Cannot be changed after declaration
Typesint, float64, string, bool, []T, map[K]VZero values are initialized automatically
Functionsfunc add(a, b int) int {}Multiple return values supported
Controlif, for, switch, deferNo while loop; for handles all iteration
Structstype User struct { Name string }Composite types with named fields
Interfacestype Reader interface { Read(p []byte) (n int, err error) }Satisfied implicitly by implementing methods
variables.go
GO
1package main
2
3import "fmt"
4
5func 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

The short declaration operator := declares a new variable only if the name does not already exist in the current scope. Reusing it with an existing variable can shadow outer declarations and lead to subtle bugs.
Control Flow

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.

control-flow.go
GO
1package main
2
3import (
4 "fmt"
5 "os"
6)
7
8func 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

Prefer switch over long if-else chains. In Go, switch cases do not fall through by default, which eliminates a common class of bugs.
Functions

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.

functions.go
GO
1package main
2
3import "fmt"
4
5// Multiple parameters of the same type
6func rectangle(width, height int) int {
7 return width * height
8}
9
10// Multiple return values
11func 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
19func 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
26func sum(numbers ...int) int {
27 total := 0
28 for _, n := range numbers {
29 total += n
30 }
31 return total
32}
33
34func 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.

Packages & Modules

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.

mathutils/mathutils.go
GO
1// mathutils/mathutils.go
2package mathutils
3
4// Add is exported because it starts with a capital letter.
5func Add(a, b int) int {
6 return a + b
7}
8
9// multiply is unexported and only visible within the package.
10func multiply(a, b int) int {
11 return a * b
12}
main-import.go
GO
1// main.go
2package main
3
4import (
5 "fmt"
6
7 "example.com/hello/mathutils"
8)
9
10func 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.

module-commands.sh
Bash
1# Add an external dependency
2go get github.com/gin-gonic/gin
3
4# Update all dependencies
5go get -u ./...
6
7# Tidy go.mod and download missing modules
8go mod tidy
9
10# List available module upgrades
11go list -m -u all
12
13# Vendor dependencies into the project
14go mod vendor

info

Run go mod tidy after adding or removing imports. It keeps go.mod and go.sum accurate and removes unused dependencies.
Structs & Methods

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.

structs.go
GO
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8type User struct {
9 ID int
10 Email string
11 CreatedAt time.Time
12}
13
14// Value receiver: does not modify the original.
15func (u User) DisplayName() string {
16 return u.Email
17}
18
19// Pointer receiver: can modify the original.
20func (u *User) Verify() {
21 u.ID = -u.ID
22}
23
24type Admin struct {
25 User // embedded struct
26 Role string
27}
28
29func 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

Use value receivers for small, immutable types and pointer receivers when the method must mutate state or when the struct is large. Be consistent: if one method on a type uses a pointer receiver, the rest usually should too.
Interfaces

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.

interfaces.go
GO
1package main
2
3import "fmt"
4
5// Small, focused interface — the Go way.
6type Greeter interface {
7 Greet() string
8}
9
10type Person struct {
11 Name string
12}
13
14func (p Person) Greet() string {
15 return "Hello, " + p.Name
16}
17
18type Robot struct {
19 Model string
20}
21
22func (r Robot) Greet() string {
23 return "Beep boop, " + r.Model
24}
25
26func sayHello(g Greeter) {
27 fmt.Println(g.Greet())
28}
29
30func 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

The standard library's most powerful interfaces are tiny: io.Reader, io.Writer, fmt.Stringer. Follow this pattern in your own code: interfaces with one or two methods are easier to implement and compose.
Error Handling

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.

errors.go
GO
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8var ErrNotFound = errors.New("resource not found")
9
10func 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
20func 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.

custom-errors.go
GO
1type ValidationError struct {
2 Field string
3}
4
5func (e *ValidationError) Error() string {
6 return fmt.Sprintf("validation failed for field %q", e.Field)
7}
8
9func saveUser(name string) error {
10 if name == "" {
11 return &ValidationError{Field: "name"}
12 }
13 return nil
14}
15
16func main() {
17 err := saveUser("")
18 var validationErr *ValidationError
19 if errors.As(err, &validationErr) {
20 fmt.Println(validationErr.Field)
21 }
22}

danger

Never ignore errors with the blank identifier unless you explicitly intend to discard them. Silent failures are the most common source of Go bugs in production.
Concurrency

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.

concurrency.go
GO
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func 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
16func 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.

sync.go
GO
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8func 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

Goroutine closures capture loop variables by reference. Always pass loop variables as parameters to the goroutine to avoid race conditions and unexpected values.
Tooling

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.

CommandPurpose
go buildCompile packages and dependencies
go runCompile and run a program
go testRun tests and benchmarks
go fmtFormat source files
go vetStatic analysis for likely mistakes
go modDependency and module management
go docShow documentation for packages and symbols
go generateRun code generators via //go:generate directives
tooling.sh
Bash
1# Format all files in the current module
2go fmt ./...
3
4# Run tests with coverage
5go test -cover ./...
6
7# Run benchmarks
8go test -bench=. -benchmem ./...
9
10# Static analysis
11go vet ./...
12
13# Build with race detector
14go run -race main.go
15
16# Profile a program
17go test -cpuprofile=cpu.out ./...
18go 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.

add_test.go
GO
1package mathutils
2
3import "testing"
4
5func 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

Configure your editor to run go fmt on save. Consistent formatting eliminates style debates and makes diffs easier to read.
Ecosystem

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.

CategoryPopular Choices
Web frameworksGin, Echo, Chi, Fiber, standard library net/http
Databasesdatabase/sql, GORM, sqlx, pgx, mongo-driver, Redis clients
ConfigurationViper, envconfig, caarlos0/env
CLIcobra, urfave/cli, spf13/viper
ObservabilityOpenTelemetry, Zap, slog, Prometheus client
Testingtestify, gomock, httpexpect, fuzzing in Go 1.18+
Cloud SDKsAWS SDK for Go v2, Google Cloud Go, Azure SDK
gin-example.go
GO
1package main
2
3import (
4 "net/http"
5
6 "github.com/gin-gonic/gin"
7)
8
9func 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

For many services, the standard library's net/http is sufficient. Reach for frameworks like Gin or Echo when you need routing helpers, middleware chaining, or higher request throughput.
Best Practices

Writing idiomatic Go means embracing simplicity and explicitness. The following practices will keep your code clean, testable, and maintainable as your project grows.

DoDon't
Handle errors explicitly at every layerIgnore errors with _ or panic for routine failures
Keep interfaces small and focusedCreate large interfaces with many methods
Use composition to share behaviorSimulate inheritance with deep type hierarchies
Write table-driven testsCopy-paste nearly identical test cases
Document exported symbols with commentsLeave public APIs undocumented
Use context for cancellation and deadlinesRely on goroutines without lifecycle management
Run gofmt, go vet, and go test in CIMerge code without static analysis or tests
context.go
GO
1package main
2
3import (
4 "context"
5 "fmt"
6 "time"
7)
8
9func 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
21func main() {
22 if err := fetchWithTimeout(context.Background()); err != nil {
23 fmt.Println("failed:", err)
24 }
25}

best practice

Pass context.Context as the first parameter to functions that perform I/O, network calls, or long-running work. It enables cancellation, timeouts, and request-scoped values without global state.
$Blueprint — Engineering Documentation·Section ID: GO-OVERVIEW·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.