Go (Golang) Tutorial
Go, often called Golang, is an open-source, compiled programming language designed for building simple, reliable and efficient software. It combines the speed and type safety of a compiled language with a small, easy-to-read syntax, fast builds, built-in concurrency and a garbage collector. Much of today's cloud infrastructure, including Docker, Kubernetes and Terraform, is written in Go.
This tutorial teaches Go from scratch: installing the toolchain, modules and packages, variables and types, control flow, functions and error handling, arrays, slices and maps, structs, methods and interfaces, generics, goroutines and channels, building an HTTP server, testing and the Go tool chain. Every section includes small programs you can run.
1. What Is Go?
Go is a statically typed, compiled language with garbage collection and first-class support for concurrency. A Go program compiles into a single native executable with no external runtime to install, which makes it very easy to deploy, for example in a small container image.
Go was designed to make large software projects easier to manage. Its creators deliberately kept the language small: there are only 25 keywords, one loop statement, no classes or inheritance, no exceptions and one standard code format. The result is code that looks similar no matter who wrote it, which helps teams read and maintain each other's work.
Go is especially popular for:
- Cloud and infrastructure tools: Docker, Kubernetes, Terraform, Prometheus and etcd are written in Go.
- Web services and APIs: fast HTTP servers and microservices with a small memory footprint.
- Command-line tools: single binaries that run anywhere without dependencies.
- Networking and distributed systems: proxies, load balancers and message processing.
- DevOps and site reliability tooling: automation, monitoring agents and deployment tools.
2. A Short History of Go
Go was designed at Google in 2007 by Robert Griesemer, Rob Pike and Ken Thompson. They were frustrated by slow builds and the complexity of large C++ code bases, and wanted a language that was fast to compile, simple to read and well suited to multicore machines and networked services.
| Year | Milestone |
|---|---|
| 2007 | Design work began at Google. |
| 2009 | Go was announced publicly as an open-source project. |
| 2012 | Go 1.0 was released with the Go 1 compatibility promise: programs written for Go 1 keep compiling with later 1.x versions. |
| 2015 | Go 1.5: the compiler and runtime were rewritten in Go, and garbage-collection pauses became much shorter. |
| 2018 | Go 1.11 introduced modules for dependency management. |
| 2022 | Go 1.18 added generics (type parameters) and built-in fuzz testing. |
| 2024 | Go 1.22 gave each loop iteration its own variable and added method and path patterns to the HTTP router; Go 1.23 added range-over-function iterators. |
A new Go version is released about every six months, usually in February and August. The two most recent versions are supported with security fixes.
3. Key Features of Go
| Feature | What it means for you |
|---|---|
| Simple syntax | A small language that can be learned quickly and read easily. |
| Fast compilation | Even large projects build in seconds, keeping the edit-run cycle short. |
| Single static binary | Deploy one file; no virtual machine or interpreter is required. |
| Goroutines and channels | Lightweight concurrency built into the language. |
| Garbage collection | Automatic memory management with very short pauses. |
| Strong standard library | HTTP servers and clients, JSON, cryptography, testing and more, without third-party packages. |
| Built-in tooling | Formatting, testing, benchmarking, profiling, vetting and documentation are part of the go command. |
| Easy cross-compilation | Build for Linux, Windows or macOS on any CPU from a single machine. |
4. Installing Go and Your First Program
Download the installer for your operating system from the official Go website (go.dev), or use a package manager such as Homebrew (brew install go). Check the installation with go version. Visual Studio Code with the Go extension and GoLand are popular editors.
# Create a project folder and a module
mkdir hello && cd hello
go mod init example.com/hello
# After creating main.go (below), run it or build a binary
go run .
go build -o hello . && ./hello
package main
import (
"fmt"
"time"
)
func main() {
name := "Gopher"
fmt.Printf("Hello, %s! Today is %s.\n", name, time.Now().Format("Monday"))
}
- Every Go file starts with a
packageclause. An executable program must have amainpackage with amainfunction. importlists the packages used. Go refuses to compile if an import or a local variable is unused, which keeps code tidy.:=declares a variable and infers its type from the value.- There are no semicolons at line ends; the compiler inserts them automatically.
5. Modules and Packages
A package is a directory of .go files that are compiled together. A module is a collection of packages released together, defined by a go.mod file that records the module path, the Go version and the dependencies.
Visibility is controlled by capitalisation: names starting with an upper-case letter (such as Area) are exported and visible to other packages, while lower-case names (area) are private to their package. There are no public or private keywords.
# Add a third-party dependency (recorded in go.mod and go.sum)
go get github.com/google/uuid@latest
# Remove unused dependencies and add missing ones
go mod tidy
The go.sum file stores cryptographic checksums of each dependency, so builds are reproducible and tampered packages are detected. Packages are downloaded through a public module proxy and checked against a checksum database by default.
6. Variables, Types and Constants
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota // iota counts up: 0, 1, 2, ...
Monday
Tuesday
)
const Pi = 3.14159
func main() {
var city string = "Bengaluru" // explicit type
var population = 13_600_000 // type inferred as int
rating := 4.7 // short declaration inside functions
var active bool // zero value: false
var count int // zero value: 0
var label string // zero value: ""
fmt.Println(city, population, rating, active, count, label == "")
fmt.Println(Monday, Tuesday, Pi)
// Conversions are always explicit
items := 7
avg := float64(items) / 2
fmt.Printf("avg=%.1f type=%T\n", avg, avg)
// Strings are immutable UTF-8 byte sequences; runes are Unicode code points
word := "naïve"
fmt.Println(len(word), len([]rune(word))) // 6 bytes, 5 runes
}
Every type has a zero value: 0 for numbers, false for booleans, "" for strings and nil for pointers, slices, maps, channels, functions and interfaces. Variables are always initialised, so there is no "uninitialised memory" problem. Go never converts between types implicitly, even between int and int64.
7. Control Flow
Go has if, switch and a single loop keyword, for, which covers every kind of loop. Conditions need no parentheses, but braces are always required.
package main
import (
"fmt"
"strconv"
)
func main() {
// Classic three-part loop
for i := 1; i <= 3; i++ {
fmt.Print(i, " ")
}
fmt.Println()
// "while" loop
n := 10
for n > 1 {
n /= 2
}
// range over an integer (Go 1.22+), a slice and a map
for i := range 3 {
fmt.Print(i, " ")
}
for idx, fruit := range []string{"apple", "mango"} {
fmt.Println(idx, fruit)
}
// if with a short statement: the variable exists only inside the if/else
if v, err := strconv.Atoi("42"); err == nil {
fmt.Println("parsed", v)
} else {
fmt.Println("error:", err)
}
// switch: no fall-through by default, cases can list several values
switch day := "sat"; day {
case "sat", "sun":
fmt.Println("Weekend")
default:
fmt.Println("Weekday")
}
// switch with no condition is a clean if/else chain
score := 82
switch {
case score >= 90:
fmt.Println("A")
case score >= 75:
fmt.Println("B")
default:
fmt.Println("C")
}
}
8. Functions and Error Handling
Functions can return multiple values. Go uses this for error handling: a function that can fail returns its result plus an error as the last value, and the caller checks it explicitly. There are no exceptions for ordinary errors.
package main
import (
"errors"
"fmt"
"os"
)
var ErrInsufficientFunds = errors.New("insufficient funds") // a sentinel error
func withdraw(balance, amount float64) (float64, error) {
if amount <= 0 {
return balance, fmt.Errorf("invalid amount %.2f", amount)
}
if amount > balance {
// %w wraps the sentinel so callers can detect it with errors.Is
return balance, fmt.Errorf("withdraw %.2f: %w", amount, ErrInsufficientFunds)
}
return balance - amount, nil
}
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open config: %w", err)
}
defer f.Close() // runs when readConfig returns, whatever path it takes
buf := make([]byte, 512)
n, err := f.Read(buf)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
return buf[:n], nil
}
func main() {
balance, err := withdraw(100, 250)
if errors.Is(err, ErrInsufficientFunds) {
fmt.Println("Declined:", err)
}
fmt.Println("Balance:", balance)
if _, err := readConfig("missing.yaml"); err != nil {
fmt.Println(err) // open config: open missing.yaml: no such file or directory
}
// Functions are values; closures capture surrounding variables
counter := func() func() int {
c := 0
return func() int { c++; return c }
}()
fmt.Println(counter(), counter(), counter()) // 1 2 3
}
defer schedules a call to run when the surrounding function returns, which is the standard way to close files, unlock mutexes and release resources. panic and recover exist for truly unrecoverable situations, such as programming bugs, but normal failures should always be returned as error values.
9. Arrays, Slices and Maps
An array has a fixed length that is part of its type ([5]int). In practice you mostly use slices: flexible views onto an underlying array, which can grow with append. A slice is a small structure holding a pointer to the array, a length and a capacity.
package main
import (
"fmt"
"maps"
"slices"
"sort"
)
func main() {
arr := [6]int{10, 20, 30, 40, 50, 60}
s := arr[1:4]
fmt.Println(s, len(s), cap(s)) // [20 30 40] 3 5
s[0] = 99 // shares memory with arr
fmt.Println(arr[1]) // 99
var tasks []string // nil slice, ready to append to
tasks = append(tasks, "write", "test", "deploy")
fmt.Println(tasks, len(tasks))
scores := make([]int, 0, 10) // length 0, capacity 10: no reallocation for 10 appends
scores = append(scores, 72, 95, 88)
slices.Sort(scores)
fmt.Println(scores, slices.Contains(scores, 95))
copyOf := slices.Clone(scores) // an independent copy
copyOf[0] = 0
fmt.Println(scores[0], copyOf[0])
// Maps
stock := map[string]int{"pen": 40, "eraser": 15}
stock["notebook"] = 25
delete(stock, "eraser")
if qty, ok := stock["pencil"]; !ok { // "comma ok" tells missing from zero
fmt.Println("no pencils, qty =", qty)
}
keys := slices.Collect(maps.Keys(stock))
sort.Strings(keys) // map iteration order is random; sort for stable output
for _, k := range keys {
fmt.Println(k, stock[k])
}
}
The slices and maps packages in the standard library provide generic helpers for sorting, searching, cloning and collecting keys. Map iteration order is intentionally random, so never rely on it.
10. Structs, Methods and Pointers
Go has no classes. Instead, you define structs to group data and attach methods to any named type. Code reuse comes from embedding one struct in another rather than inheritance.
package main
import (
"encoding/json"
"fmt"
"time"
)
type Audit struct {
CreatedAt time.Time `json:"created_at"`
}
func (a Audit) Age() time.Duration { return time.Since(a.CreatedAt) }
type Account struct {
Audit // embedded: Account gets Audit's fields and methods
Owner string `json:"owner"`
Balance float64 `json:"balance"`
}
// Pointer receiver: the method can modify the account
func (a *Account) Deposit(amount float64) {
a.Balance += amount
}
// Value receiver: works on a copy, fine for read-only methods
func (a Account) Summary() string {
return fmt.Sprintf("%s has %.2f", a.Owner, a.Balance)
}
func NewAccount(owner string) *Account { // constructor function by convention
return &Account{Audit: Audit{CreatedAt: time.Now()}, Owner: owner}
}
func main() {
acc := NewAccount("Priya")
acc.Deposit(1500)
fmt.Println(acc.Summary())
fmt.Println(acc.Age() < time.Second) // promoted method from Audit
data, _ := json.Marshal(acc) // struct tags control JSON field names
fmt.Println(string(data))
p := &acc.Balance // pointers: & takes an address, * dereferences
*p += 100
fmt.Println(acc.Balance)
}
Use a pointer receiver (*Account) when a method changes the struct or the struct is large; use a value receiver for small, read-only types. Go has pointers but no pointer arithmetic, so they are safe to use.
11. Interfaces
An interface is a set of method signatures. A type satisfies an interface implicitly, simply by having those methods; there is no implements keyword. This makes it easy to define small interfaces where they are used and to swap real implementations for test doubles.
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
Name() string
}
type Circle struct{ R float64 }
type Rect struct{ W, H float64 }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (c Circle) Name() string { return "circle" }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Name() string { return "rectangle" }
// fmt.Stringer is a standard one-method interface
func (r Rect) String() string { return fmt.Sprintf("Rect(%gx%g)", r.W, r.H) }
func totalArea(shapes ...Shape) float64 {
sum := 0.0
for _, s := range shapes {
sum += s.Area()
}
return sum
}
func main() {
shapes := []Shape{Circle{1.5}, Rect{2, 3.5}}
for _, s := range shapes {
fmt.Printf("%-10s %.2f\n", s.Name(), s.Area())
}
fmt.Printf("total %.2f\n", totalArea(shapes...))
// Type switch: act on the concrete type behind an interface value
for _, s := range shapes {
switch v := s.(type) {
case Rect:
fmt.Println("found", v) // uses String()
case Circle:
fmt.Println("circle with radius", v.R)
}
}
}
The standard library is built around tiny interfaces such as io.Reader, io.Writer, error and fmt.Stringer. A common Go saying is "accept interfaces, return structs": functions take the smallest interface they need and return concrete types.
12. Generics
Since Go 1.18, functions and types can have type parameters. A constraint (an interface) states which types are allowed.
package main
import (
"cmp"
"fmt"
)
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
func Map[T, U any](in []T, f func(T) U) []U {
out := make([]U, 0, len(in))
for _, v := range in {
out = append(out, f(v))
}
return out
}
func MaxOf[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// A generic type: a simple stack
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{0.5, 1.25}))
fmt.Println(Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprint(n * n) }))
fmt.Println(MaxOf("apple", "banana"), MaxOf(3, 7))
var st Stack[string]
st.Push("a")
st.Push("b")
top, _ := st.Pop()
fmt.Println(top)
}
Use generics for data structures and algorithms that genuinely work with many types. For behaviour that varies by type, plain interfaces are often simpler.
13. Goroutines and Channels
A goroutine is a function running concurrently with others, started with the go keyword. Goroutines are extremely cheap, starting with only a few kilobytes of stack, so a program can run hundreds of thousands of them. The Go runtime schedules them onto a small number of operating-system threads.
Goroutines communicate through channels, typed pipes that safely pass values between them. Go's motto is: "Do not communicate by sharing memory; instead, share memory by communicating."
package main
import (
"fmt"
"sync"
"time"
)
// A worker pool: 3 goroutines process jobs from one channel
func worker(id int, jobs <-chan int, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for n := range jobs { // loop ends when the jobs channel is closed
time.Sleep(100 * time.Millisecond) // simulate work
results <- fmt.Sprintf("worker %d: %d squared is %d", id, n, n*n)
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan string, 10)
var wg sync.WaitGroup
for id := 1; id <= 3; id++ {
wg.Add(1)
go worker(id, jobs, results, &wg)
}
for n := 1; n <= 6; n++ {
jobs <- n
}
close(jobs) // no more jobs: workers exit their loops
go func() {
wg.Wait() // wait for all workers ...
close(results) // ... then close results so the loop below ends
}()
for r := range results {
fmt.Println(r)
}
// select waits on several channel operations, here with a timeout
slow := make(chan string)
go func() { time.Sleep(2 * time.Second); slow <- "done" }()
select {
case msg := <-slow:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("timed out")
}
}
An unbuffered channel (make(chan T)) makes the sender wait until a receiver is ready, synchronising the two goroutines. A buffered channel (make(chan T, n)) lets senders continue until the buffer is full. Only the sender should close a channel, and only when receivers need to know that no more values will come.
14. Synchronisation and Context
Channels are not the only tool. The sync package provides Mutex, RWMutex, WaitGroup and Once, and sync/atomic offers atomic counters. The context package carries cancellation signals and deadlines across goroutines and API boundaries, which is essential in servers.
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
type Counter struct {
mu sync.Mutex
hits map[string]int
}
func (c *Counter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.hits[key]++
}
func fetchReport(ctx context.Context) (string, error) {
select {
case <-time.After(2 * time.Second): // pretend this is a slow database query
return "report ready", nil
case <-ctx.Done():
return "", ctx.Err() // context.DeadlineExceeded or context.Canceled
}
}
func main() {
c := Counter{hits: map[string]int{}}
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() { defer wg.Done(); c.Inc("/home") }()
}
wg.Wait()
fmt.Println("hits:", c.hits["/home"]) // always 100
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
if _, err := fetchReport(ctx); errors.Is(err, context.DeadlineExceeded) {
fmt.Println("gave up: report took too long")
}
}
go test -race ./... or go run -race .. It reports any place where goroutines access the same memory without synchronisation.15. Building a Web Server
Go's standard library includes a production-quality HTTP server. Since Go 1.22, the built-in router supports HTTP methods and path parameters, so many small services need no framework at all.
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
)
type Book struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
}
type Store struct {
mu sync.RWMutex
books map[string]Book
}
func (s *Store) getBook(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // path parameter from the route pattern
s.mu.RLock()
book, ok := s.books[id]
s.mu.RUnlock()
if !ok {
http.Error(w, "book not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(book)
}
func (s *Store) addBook(w http.ResponseWriter, r *http.Request) {
var b Book
if err := json.NewDecoder(r.Body).Decode(&b); err != nil || b.ID == "" {
http.Error(w, "invalid book", http.StatusBadRequest)
return
}
s.mu.Lock()
s.books[b.ID] = b
s.mu.Unlock()
w.WriteHeader(http.StatusCreated)
}
func main() {
store := &Store{books: map[string]Book{
"1": {ID: "1", Title: "Learning Go", Author: "A. Gopher"},
}}
mux := http.NewServeMux()
mux.HandleFunc("GET /books/{id}", store.getBook)
mux.HandleFunc("POST /books", store.addBook)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second, // always set timeouts in production
WriteTimeout: 10 * time.Second,
}
log.Println("listening on http://localhost:8080")
log.Fatal(srv.ListenAndServe())
}
Run it with go run server.go, then try curl localhost:8080/books/1. Each request is handled in its own goroutine automatically, which is why the map is protected with a read-write mutex.
16. Testing in Go
Testing is built in. Test files end in _test.go, test functions start with Test and take a *testing.T. The idiomatic style is the table-driven test:
package pricing
import "testing"
func ApplyDiscount(price, percent float64) float64 {
if percent < 0 || percent > 100 {
return price
}
return price * (1 - percent/100)
}
func TestApplyDiscount(t *testing.T) {
tests := []struct {
name string
price, percent float64
want float64
}{
{"ten percent", 100, 10, 90},
{"no discount", 59.99, 0, 59.99},
{"invalid percent", 80, 150, 80},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := ApplyDiscount(tc.price, tc.percent); got != tc.want {
t.Errorf("ApplyDiscount(%v, %v) = %v, want %v", tc.price, tc.percent, got, tc.want)
}
})
}
}
func BenchmarkApplyDiscount(b *testing.B) {
for b.Loop() { // Go 1.24+; older versions use: for i := 0; i < b.N; i++
ApplyDiscount(100, 10)
}
}
Run go test ./... for all packages, go test -cover for coverage, go test -bench . for benchmarks and go test -race to detect data races. The net/http/httptest package makes it easy to test HTTP handlers without starting a real server.
17. The Go Toolchain
| Command | What it does |
|---|---|
go run . | Compile and run the program in one step. |
go build | Compile a binary. |
go fmt ./... / gofmt | Format code in the one standard style. |
go vet ./... | Report suspicious code, such as wrong Printf arguments. |
go test ./... | Run tests, benchmarks, examples and fuzz tests. |
go mod tidy | Clean up module dependencies. |
go doc fmt.Printf | Show documentation for a package or symbol. |
go tool pprof | Analyse CPU and memory profiles. |
Cross-compiling needs only two environment variables. For example, GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 . builds a Linux ARM binary from a Mac or Windows machine. Popular extra tools include golangci-lint, which runs many linters at once, and staticcheck.
18. Go vs. Java
| Aspect | Go | Java |
|---|---|---|
| Compilation | Native binary, compiled ahead of time | Bytecode run on the JVM with JIT compilation |
| Deployment | One self-contained executable | JAR plus a Java runtime (or a native image with GraalVM) |
| Startup and memory | Fast startup, small memory footprint | Slower startup and higher baseline memory, excellent long-run throughput |
| Object model | Structs, methods, implicit interfaces, embedding | Classes, inheritance, explicit interfaces |
| Error handling | Errors as return values | Exceptions |
| Concurrency | Goroutines and channels in the language | Threads, executors and virtual threads in the library |
| Language size | Small, deliberately minimal | Large and feature-rich |
| Ecosystem | Strong for cloud, CLI and networking | Huge, mature enterprise ecosystem (Spring, Jakarta EE) |
Both are excellent for back-end services. Go shines for small, fast, easily deployed services and infrastructure tools; Java shines for large business applications with rich frameworks. Compare with the Java tutorial and the Microservices tutorial on this site.
19. Best Practices and Common Mistakes
Best practices
- Run
gofmtandgo veton every save or commit. - Handle every error, and wrap it with context using
fmt.Errorf("doing X: %w", err). - Keep interfaces small and define them where they are used.
- Pass
context.Contextas the first parameter of functions that do I/O or may be cancelled. - Make zero values useful, so structs work without special initialisation where possible.
- Know when each goroutine will stop, using contexts or closed channels.
- Write table-driven tests and run them with
-race. - Choose clear, short names; Go favours
bufandsrvin small scopes and descriptive names for exported identifiers.
Common mistakes to avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
Ignoring errors with _ | Failures go unnoticed and cause confusing bugs later. | Check and handle or return every error. |
| Writing to a nil map | The program panics. | Create maps with make or a literal before writing. |
| Goroutine leaks | Goroutines blocked forever on a channel pile up and use memory. | Use context cancellation, buffered channels or close channels properly. |
| Sharing maps between goroutines without locks | Data races and crashes ("concurrent map writes"). | Protect with a mutex or use channels. |
| Assuming appended slices are independent | Two slices may still share the same underlying array. | Use slices.Clone when you need a separate copy. |
Using panic for normal errors | Crashes the program instead of letting callers decide. | Return an error. |
| No timeouts on HTTP servers and clients | Slow clients or servers tie up resources indefinitely. | Set server timeouts and use contexts or client timeouts. |
20. Summary
- Go is a small, statically typed, compiled language with garbage collection, built for simple and reliable software.
- Programs are organised into packages and modules; capitalised names are exported.
- Variables always have zero values, conversions are explicit, and
foris the only loop. - Functions return multiple values, and errors are ordinary values checked explicitly;
deferhandles cleanup. - Slices and maps are the everyday collections; slices are views onto shared arrays.
- Structs, methods, embedding and implicit interfaces replace classes and inheritance; generics cover type-independent code.
- Goroutines and channels make concurrency simple;
syncandcontexthandle locking and cancellation. - The standard library and the
gocommand cover HTTP servers, JSON, testing, formatting and cross-compilation.
A great next step is to extend the book server above with a database, structured logging using log/slog and table-driven tests, then build it as a single binary in a small container image. Open the FAQ tab for answers to the questions learners most often ask about Go.
Frequently Asked Questions (FAQ)
Q1. Is it called Go or Golang?
The official name is Go. "Golang" comes from the project's original website address, golang.org, and is widely used because it is easier to search for. Both refer to the same language.
Q2. Is Go easy to learn?
Yes, Go is one of the easier compiled languages to learn. It has a small specification, only 25 keywords, one loop construct and one standard formatting style, so there are fewer features and styles to master. Developers who know Java, C or Python can usually write useful Go within days.
The concepts that take more practice are interfaces, pointers versus values, and designing concurrent code with goroutines and channels.
Q3. What is Go mainly used for?
Go is most popular for cloud-native infrastructure (Docker, Kubernetes, Terraform, Prometheus), back-end web services and APIs, microservices, command-line tools, networking software and DevOps tooling. Its fast startup, small binaries and built-in concurrency suit containers and servers handling many simultaneous connections.
Q4. Is Go object-oriented?
Partly. Go has types with methods, encapsulation through exported and unexported names, and polymorphism through interfaces. It deliberately has no classes and no inheritance. Instead, you build behaviour by composition, embedding one struct in another, and by implementing small interfaces implicitly.
Q5. Why does Go not have exceptions?
Go's designers wanted errors to be visible in the code. Returning an error value forces each caller to decide what to do, which makes control flow explicit and easy to follow. Wrapping errors with %w keeps context, and errors.Is and errors.As let callers inspect them.
For truly exceptional situations, such as programming bugs, Go has panic and recover, but they are not used for ordinary failures.
Q6. What is a goroutine?
A goroutine is a lightweight thread managed by the Go runtime rather than the operating system. You start one by putting go before a function call. Goroutines start with a small stack that grows as needed, so a program can run hundreds of thousands of them, and the runtime schedules them across a few OS threads.
Q7. What is the difference between buffered and unbuffered channels?
An unbuffered channel has no storage: a send waits until another goroutine receives, so the two goroutines synchronise at that moment. A buffered channel, created with make(chan T, n), can hold up to n values; sends only wait when the buffer is full, and receives only wait when it is empty.
Q8. Should I use channels or mutexes?
Use channels to pass data or ownership between goroutines, coordinate stages of a pipeline or signal events such as completion and cancellation. Use a mutex to protect shared state that several goroutines read and update, such as a cache or counter. Both are idiomatic; pick the one that makes the code simplest.
Q9. What is the difference between an array and a slice?
An array has a fixed length that is part of its type, such as [4]int, and assigning it copies all the elements. A slice is a lightweight descriptor (pointer, length and capacity) that refers to part of an underlying array and can grow with append. Copying a slice copies only the descriptor, so both copies share the same data.
Slices are what you use almost all the time.
Q10. When should I use a pointer receiver?
Use a pointer receiver (func (a *Account)) when the method must modify the value, when the struct is large and copying it would be wasteful, or when it contains a field that must not be copied, such as a sync.Mutex. Use value receivers for small, immutable types. Keep receivers consistent across a type's methods.
Q11. Does Go have generics?
Yes, since Go 1.18 (2022). Functions and types can declare type parameters with constraints, for example func Max[T cmp.Ordered](a, b T) T. The standard library uses them in the slices, maps and cmp packages. Go's generics are intentionally simpler than those of C++ or Java.
Q12. How does Go manage memory?
Go has a concurrent garbage collector that runs alongside your program with very short pauses. The compiler also performs escape analysis: values that do not outlive a function are placed on the stack, which is cheaper than heap allocation and creates no garbage.
You can tune the collector with the GOGC and GOMEMLIMIT settings, and profile memory with pprof.
Q13. Is Go faster than Java or Python?
Go is typically much faster than Python for CPU-bound code because it compiles to native machine code. Compared with Java, results depend on the workload: Go usually starts faster and uses less memory, while the JVM's JIT compiler can match or beat Go in long-running, heavily optimised throughput. Both are fast enough for most back-end services.
Q14. Do I need a web framework for Go?
Often not. The standard net/http package includes a production-ready server, and since Go 1.22 its router supports HTTP methods and path parameters. Many teams build APIs with the standard library plus a few focused packages. Frameworks such as Gin, Echo and Chi add conveniences like middleware collections and request binding if you want them.
Q15. What does the context package do?
context.Context carries deadlines, cancellation signals and request-scoped values across function calls and goroutines. When a client disconnects or a timeout expires, the context is cancelled, and any database query, HTTP call or goroutine watching it can stop early and free resources. By convention it is the first parameter of a function, named ctx.
Q16. How do I manage dependencies in Go?
Go uses modules. Run go mod init to create a go.mod file, add packages with go get, and run go mod tidy to keep the dependency list accurate. The go.sum file records checksums so every build uses exactly the same, verified code. Go selects the minimum versions that satisfy all requirements, which keeps builds predictable.
Q17. Can Go build programs for other operating systems?
Yes, cross-compilation is built in. Set GOOS and GOARCH before building, for example GOOS=windows GOARCH=amd64 go build or GOOS=linux GOARCH=arm64 go build. Pure Go programs need no extra tools; programs that use C code through cgo need a C cross-compiler for the target.
Q18. Why does Go refuse to compile unused variables and imports?
Unused imports slow down compilation and unused variables often indicate bugs, such as assigning to the wrong variable. Making them compile errors keeps code clean. During development, you can assign a value to the blank identifier _ to silence the error temporarily, and editors with goimports add and remove imports automatically.
Q19. What are Go's main drawbacks?
Common criticisms include verbose error handling (many if err != nil checks), a deliberately limited feature set, generics that are simpler than in some languages, and garbage collection, which makes it less suitable than C, C++ or Rust for hard real-time or very memory-constrained systems. Many teams see the simplicity as a strength rather than a weakness.
Q20. What are good projects to practise Go?
Good starter projects include:
- A command-line tool, such as a file organiser, word counter or to-do manager.
- A REST API with
net/http, JSON and a database. - A concurrent website link checker or web crawler using goroutines.
- A URL shortener or a simple chat server.
- A tool that calls a public API and caches results with a mutex-protected map.