Skip to content

A Comprehensive Tour of Go

A practical guide covering the most important Go concepts with real-world examples from Go by Example and the official Go Tour.


  1. Getting Started & Package Basics
  2. Variables & Basic Types
  3. Structs - Organizing Data
  4. Error Handling
  5. Reading Files
  6. Writing Files
  7. Working with Directories
  8. Goroutines - Concurrent Execution
  9. Channels - Communicating Between Goroutines
  10. HTTP Client - GET & POST Requests
  11. HTTP Server
  12. Context for Request Management
  13. JSON Encoding & Decoding

Every Go program starts with a package declaration and imports.

package main
import "fmt"
func main() {
fmt.Println("hello world")
}

Key Points:

  • package main declares the main package (entry point for executables)
  • import "fmt" imports the formatting I/O package
  • main() function is the program’s entry point

Running Go Programs:

Terminal window
# Run directly without building
go run hello-world.go
# Build an executable binary
go build hello-world.go
./hello-world

In Go, variables are explicitly declared and used by the compiler to check type-correctness of function calls.

package main
import "fmt"
func main() {
// Basic var declaration with initialization
var a = "initial"
fmt.Println(a)
// Multiple variables at once
var b, c int = 1, 2
fmt.Println(b, c)
// Type inference - Go infers the type
var d = true
fmt.Println(d)
// Zero values - variables without initialization
var e int
fmt.Println(e) // Prints: 0
// Shorthand := syntax (only inside functions)
f := "apple"
fmt.Println(f)
}

Output:

initial
1 2
true
0
apple

Key Concepts:

  • Use var for explicit declarations
  • Go infers types from initialization values
  • Zero-valued: Variables without initialization get default values (0 for int, "" for string, false for bool)
  • := shorthand combines declaration and initialization (function-scope only)

Structs are typed collections of fields used to group related data.

package main
import "fmt"
type person struct {
name string
age int
}
// Constructor function (idiomatic pattern)
func newPerson(name string) *person {
p := person{name: name}
p.age = 42
return &p // Safe to return pointer to local variable
}
func main() {
// Positional initialization
fmt.Println(person{"Bob", 20})
// Named field initialization
fmt.Println(person{name: "Alice", age: 30})
// Omitted fields are zero-valued
fmt.Println(person{name: "Fred"}) // age = 0
// Pointer to struct
fmt.Println(&person{name: "Ann", age: 40})
// Using constructor
fmt.Println(newPerson("Jon"))
// Access fields with dot notation
s := person{name: "Sean", age: 50}
fmt.Println(s.name)
// Pointers automatically dereference
sp := &s
fmt.Println(sp.age)
// Structs are mutable
sp.age = 51
fmt.Println(sp.age)
// Anonymous structs (for single-use cases)
dog := struct {
name string
isGood bool
}{
"Rex",
true,
}
fmt.Println(dog)
}

In Go, errors are the last return value and have type error, a built-in interface. This differs from exception-based languages.

package main
import (
"errors"
"fmt"
)
// Function that returns an error
func f(arg int) (int, error) {
if arg == 42 {
return -1, errors.New("can't work with 42")
}
return arg + 3, nil // nil means no error
}
// Sentinel errors (pre-declared error variables)
var ErrOutOfTea = errors.New("no more tea available")
var ErrPower = errors.New("can't boil water")
func makeTea(arg int) error {
if arg == 2 {
return ErrOutOfTea
} else if arg == 4 {
// Error wrapping with %w
return fmt.Errorf("making tea: %w", ErrPower)
}
return nil
}
func main() {
// Check errors inline
for i := 0; i < 3; i++ {
if r, e := f(i); e != nil {
fmt.Println("f failed:", e)
} else {
fmt.Println("f worked:", r)
}
}
// Check specific errors with errors.Is()
for i := range 5 {
if err := makeTea(i); err != nil {
// errors.Is() checks the error chain
if errors.Is(err, ErrOutOfTea) {
fmt.Println("We should buy new tea!")
} else if errors.Is(err, ErrPower) {
fmt.Println("Now it is dark.")
} else {
fmt.Printf("unknown error: %s\n", err)
}
continue
}
fmt.Println("Tea is ready!")
}
}

Key Points:

  • Return errors as the last value
  • nil indicates no error
  • Use errors.New() for simple errors
  • Sentinel errors: Pre-declared error variables for specific conditions
  • Error wrapping: Use fmt.Errorf() with %w to add context
  • errors.Is(): Check for specific errors in the error chain

Create custom error types by implementing the Error() method.

package main
import (
"errors"
"fmt"
)
// Custom error type with additional fields
type argError struct {
arg int
message string
}
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.message)
}
func f(arg int) (int, error) {
if arg == 42 {
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
_, err := f(42)
// Type-specific error handling with errors.As()
var ae *argError
if errors.As(err, &ae) {
fmt.Println(ae.arg) // 42
fmt.Println(ae.message) // can't work with it
} else {
fmt.Println("err doesn't match argError")
}
}

Key Points:

  • Custom errors typically use the “Error” suffix
  • Implement Error() string method
  • errors.As(): Check error type and convert to that type

Panic stops normal execution and causes program crash. Recover catches panics.

package main
import (
"os"
"path/filepath"
)
func main() {
// Panic with a message
panic("a problem")
// Common use: panic on unexpected errors
path := filepath.Join(os.TempDir(), "file")
_, err := os.Create(path)
if err != nil {
panic(err)
}
}

Output:

panic: a problem
goroutine 1 [running]:
main.main()
/.../panic.go:12 +0x47
...
exit status 2

Use panic for:

  • Unrecoverable errors during normal operation
  • Programming errors that should never happen

Go Philosophy: Unlike exception-based languages, Go prefers returning errors over panics for most error handling.

package main
import "fmt"
func mayPanic() {
panic("a problem")
}
func main() {
// Recover must be called in a deferred function
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
mayPanic()
fmt.Println("After mayPanic()") // This never executes
}

Output:

Recovered. Error:
a problem

Key Points:

  • recover() must be called inside a defer block
  • Catches panics and prevents program crash
  • Useful for servers that shouldn’t crash on individual errors
  • Code after panic doesn’t execute, but program continues

Defer ensures a function call is performed later, usually for cleanup.

package main
import (
"fmt"
"os"
)
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
// Always check errors, even in deferred functions
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f) // Will execute at function end
writeFile(f)
}

Output:

creating
writing
closing

Key Points:

  • Defer executes at function end
  • Declare cleanup immediately after resource acquisition
  • Check errors even in deferred functions
  • Similar to finally in other languages

package main
import (
"bufio"
"fmt"
"io"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// 1. Read entire file into memory
dat, err := os.ReadFile("/tmp/dat")
check(err)
fmt.Print(string(dat))
// 2. Open file for more control
f, err := os.Open("/tmp/dat")
check(err)
defer f.Close()
// 3. Read specific number of bytes
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1]))
// 4. Seek to a position in the file
o2, err := f.Seek(6, io.SeekStart) // Absolute position
check(err)
b2 := make([]byte, 2)
n2, err := f.Read(b2)
check(err)
fmt.Printf("%d bytes @ %d: %s\n", n2, o2, string(b2[:n2]))
// 5. Seek relative to current position
_, err = f.Seek(4, io.SeekCurrent)
check(err)
// 6. ReadAtLeast ensures minimum bytes
o3, err := f.Seek(6, io.SeekStart)
check(err)
b3 := make([]byte, 2)
n3, err := io.ReadAtLeast(f, b3, 2)
check(err)
fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))
// 7. Rewind to beginning
_, err = f.Seek(0, io.SeekStart)
check(err)
// 8. Buffered reading (efficient for multiple small reads)
r4 := bufio.NewReader(f)
b4, err := r4.Peek(5) // Look ahead without consuming
check(err)
fmt.Printf("5 bytes: %s\n", string(b4))
}

Key Methods:

  • os.ReadFile(): Read entire file into memory (simplest)
  • os.Open() + Read(): Controlled reading with specific byte counts
  • Seeking modes:
    • io.SeekStart: Absolute position from beginning
    • io.SeekCurrent: Relative to current position
    • io.SeekEnd: Relative to end (supports negative offsets)
  • io.ReadAtLeast(): Safer reading with minimum byte guarantees
  • bufio.NewReader(): Buffered reading for efficiency
    • Peek(): Examine data without consuming it

package main
import (
"bufio"
"fmt"
"os"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// 1. Write entire file at once
d1 := []byte("hello\ngo\n")
err := os.WriteFile("/tmp/dat1", d1, 0644)
check(err)
// 2. Open file for more control
f, err := os.Create("/tmp/dat2")
check(err)
// Always defer Close immediately after opening
defer f.Close()
// 3. Write byte slices
d2 := []byte{115, 111, 109, 101, 10}
n2, err := f.Write(d2)
check(err)
fmt.Printf("wrote %d bytes\n", n2)
// 4. Write strings directly
n3, err := f.WriteString("writes\n")
check(err)
fmt.Printf("wrote %d bytes\n", n3)
// 5. Sync - flush writes to stable storage
f.Sync()
// 6. Buffered writing (best for multiple writes)
w := bufio.NewWriter(f)
n4, err := w.WriteString("buffered\n")
check(err)
fmt.Printf("wrote %d bytes\n", n4)
// Flush to apply buffered writes
w.Flush()
}

Key Methods:

  • os.WriteFile(): Write entire file at once (simplest)
  • os.Create(): Open file for writing
  • Idiomatic pattern: defer f.Close() immediately after opening
  • Write(): Write byte slices
  • WriteString(): Write strings directly
  • Sync(): Flush writes to disk
  • bufio.NewWriter(): Buffered writing for performance
    • Always call Flush() to apply buffered operations

package main
import (
"fmt"
"os"
"path/filepath"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// Create a single directory
err := os.Mkdir("subdir", 0755)
check(err)
defer os.RemoveAll("subdir") // Cleanup
// Create nested directories (like mkdir -p)
err = os.MkdirAll("subdir/parent/child", 0755)
check(err)
// Read directory contents
c, err := os.ReadDir("subdir/parent")
check(err)
fmt.Println("Listing subdir/parent")
for _, entry := range c {
fmt.Println(" ", entry.Name(), entry.IsDir())
}
// Change directory
err = os.Chdir("subdir/parent/child")
check(err)
// Read current directory
c, err = os.ReadDir(".")
check(err)
fmt.Println("Listing subdir/parent/child")
for _, entry := range c {
fmt.Println(" ", entry.Name(), entry.IsDir())
}
// Change back
err = os.Chdir("../../..")
check(err)
// Recursive directory traversal
fmt.Println("Visiting subdir")
err = filepath.WalkDir("subdir", visit)
check(err)
}
func visit(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(" ", path, d.IsDir())
return nil
}

Key Functions:

  • os.Mkdir(): Create single directory
  • os.MkdirAll(): Create directory hierarchy (like mkdir -p)
  • os.ReadDir(): List directory contents as os.DirEntry slice
  • os.Chdir(): Change working directory
  • os.RemoveAll(): Delete directory tree recursively
  • filepath.WalkDir(): Traverse directories with callback function

A goroutine is a lightweight thread of execution enabling concurrent programming.

package main
import (
"fmt"
"time"
)
func f(from string) {
for i := 0; i < 3; i++ {
fmt.Println(from, ":", i)
}
}
func main() {
// Standard synchronous function call
f("direct")
// Launch goroutine with go keyword
go f("goroutine")
// Anonymous function as goroutine
go func(msg string) {
fmt.Println(msg)
}("going")
// Wait for goroutines (use WaitGroup in production)
time.Sleep(time.Second)
fmt.Println("done")
}

Key Points:

  • go keyword launches functions concurrently
  • Works with named and anonymous functions
  • Goroutines execute concurrently, output may be interleaved
  • Much lighter than OS threads
  • Use WaitGroup for proper synchronization (not time.Sleep)

9. Channels - Communicating Between Goroutines

Section titled “9. Channels - Communicating Between Goroutines”

Channels are the pipes that connect concurrent goroutines. Send values from one goroutine, receive in another.

package main
import "fmt"
func main() {
// Create a channel
messages := make(chan string)
// Send value to channel from goroutine
go func() {
messages <- "ping"
}()
// Receive value from channel
msg := <-messages
fmt.Println(msg)
}

Output: ping

Key Points:

  • Create with make(chan val-type)
  • Send: channel <- value
  • Receive: value := <-channel
  • Blocking by default: Sends and receives block until both sender and receiver are ready
  • Provides synchronization without additional mechanisms

Buffered channels accept a limited number of values without a corresponding receiver.

package main
import "fmt"
func main() {
// Create buffered channel with capacity 2
messages := make(chan string, 2)
// Send without blocking (buffer has space)
messages <- "buffered"
messages <- "channel"
// Receive later
fmt.Println(<-messages)
fmt.Println(<-messages)
}

Output:

buffered
channel

Key Difference:

  • Unbuffered: Requires immediate receiver
  • Buffered: Stores values up to capacity, enables asynchronous communication

Select lets you wait on multiple channel operations simultaneously.

package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
// Goroutine 1: sends after 1 second
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
// Goroutine 2: sends after 2 seconds
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// Select waits on multiple channels
for range 2 {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}

Output:

received one
received two

Key Benefits:

  • Wait on multiple channels concurrently
  • Total execution ~2 seconds (not 3) due to concurrent execution
  • Powerful when combined with goroutines and channels

WaitGroups coordinate the completion of multiple goroutines.

package main
import (
"fmt"
"sync"
"time"
)
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// Launch 5 workers
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment counter
go func(id int) {
defer wg.Done() // Decrement counter when done
worker(id)
}(i)
}
// Block until all workers complete
wg.Wait()
}

Important:

  • wg.Add(1): Increment counter before launching goroutine
  • wg.Done(): Decrement counter when goroutine completes
  • wg.Wait(): Block until counter reaches zero
  • Pass WaitGroup by pointer when passing to functions
  • Output order is non-deterministic (concurrent execution)

Limitation: No built-in error propagation. For advanced cases, use golang.org/x/sync/errgroup


Worker pools process jobs concurrently with a fixed number of workers.

package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "started job", j)
time.Sleep(time.Second)
fmt.Println("worker", id, "finished job", j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= numJobs; a++ {
<-results
}
}

Efficiency:

  • ~2 seconds total for 5 seconds of work
  • 3 workers process 5 jobs concurrently
  • Demonstrates parallelization benefits

Pattern:

  • Buffered channels for jobs and results
  • Fixed number of workers
  • Close jobs channel when done sending
  • Workers range over jobs channel

package main
import (
"bufio"
"fmt"
"net/http"
)
func main() {
// Simple GET request
resp, err := http.Get("https://gobyexample.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Print response status
fmt.Println("Response status:", resp.Status)
// Read response body line by line
scanner := bufio.NewScanner(resp.Body)
for i := 0; scanner.Scan() && i < 5; i++ {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
panic(err)
}
}

Key Points:

  • http.Get(): Convenient shortcut for GET requests
  • Always close response body: defer resp.Body.Close()
  • Access status: resp.Status
  • Read body with bufio.Scanner for line-by-line processing
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type RequestData struct {
Name string `json:"name"`
Email string `json:"email"`
}
type ResponseData struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func main() {
// Prepare request data
data := RequestData{
Name: "John Doe",
Email: "john@example.com",
}
// Encode to JSON
jsonData, err := json.Marshal(data)
if err != nil {
panic(err)
}
// POST request
resp, err := http.Post(
"https://httpbin.org/post",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Decode response
var result ResponseData
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Printf("Response: %+v\n", result)
}

For More Control:

package main
import (
"bytes"
"net/http"
)
func main() {
client := &http.Client{}
req, err := http.NewRequest("POST", "https://api.example.com/data",
bytes.NewBuffer(jsonData))
if err != nil {
panic(err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
// Execute request
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}

package main
import (
"fmt"
"net/http"
)
// Handler functions take ResponseWriter and Request
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func headers(w http.ResponseWriter, req *http.Request) {
// Access request headers
for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}
func main() {
// Register handlers
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
// Start server on port 8090
http.ListenAndServe(":8090", nil)
}

Usage:

Terminal window
# Run server
go run http-server.go &
# Test endpoints
curl localhost:8090/hello
# Output: hello
curl localhost:8090/headers
# Output: User-Agent: curl/7.79.1
# Accept: */*

Key Concepts:

  • Handlers: Functions implementing func(http.ResponseWriter, *http.Request)
  • http.HandleFunc(): Register route handlers
  • http.ListenAndServe(): Start server on specified port
  • http.ResponseWriter: Write HTTP responses
  • *http.Request: Access request data (headers, body, etc.)

Context carries deadlines, cancellation signals, and request-scoped values across API boundaries and goroutines.

package main
import (
"fmt"
"net/http"
"time"
)
func hello(w http.ResponseWriter, req *http.Request) {
// Get context from request
ctx := req.Context()
fmt.Println("server: hello handler started")
defer fmt.Println("server: hello handler ended")
// Monitor context cancellation
select {
case <-time.After(10 * time.Second):
fmt.Fprintf(w, "hello\n")
case <-ctx.Done():
// Client disconnected or timeout
err := ctx.Err()
fmt.Println("server:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8090", nil)
}

How It Works:

  • Each HTTP request has a context via req.Context()
  • Context cancels when client disconnects
  • Use select with ctx.Done() to handle cancellation
  • ctx.Err() returns cancellation reason
  • Prevents wasting resources on abandoned requests

Test with:

Terminal window
curl localhost:8090/hello
# Press Ctrl+C before 10 seconds
# Server logs: server: context canceled

Go provides built-in JSON support through encoding/json.

package main
import (
"encoding/json"
"fmt"
"os"
)
// Structs for JSON mapping
type response1 struct {
Page int
Fruits []string
}
type response2 struct {
Page int `json:"page"`
Fruits []string `json:"fruits"`
}
func main() {
// Encode basic types
bolB, _ := json.Marshal(true)
fmt.Println(string(bolB)) // true
intB, _ := json.Marshal(1)
fmt.Println(string(intB)) // 1
fltB, _ := json.Marshal(2.34)
fmt.Println(string(fltB)) // 2.34
strB, _ := json.Marshal("gopher")
fmt.Println(string(strB)) // "gopher"
// Encode slices
slcD := []string{"apple", "peach", "pear"}
slcB, _ := json.Marshal(slcD)
fmt.Println(string(slcB)) // ["apple","peach","pear"]
// Encode maps
mapD := map[string]int{"apple": 5, "lettuce": 7}
mapB, _ := json.Marshal(mapD)
fmt.Println(string(mapB)) // {"apple":5,"lettuce":7}
// Encode structs
res1D := &response1{
Page: 1,
Fruits: []string{"apple", "peach", "pear"},
}
res1B, _ := json.Marshal(res1D)
fmt.Println(string(res1B))
// Custom field names with tags
res2D := &response2{
Page: 1,
Fruits: []string{"apple", "peach", "pear"},
}
res2B, _ := json.Marshal(res2D)
fmt.Println(string(res2B))
// Decode JSON into interface{}
byt := []byte(`{"num":6.13,"strs":["a","b"]}`)
var dat map[string]interface{}
if err := json.Unmarshal(byt, &dat); err != nil {
panic(err)
}
fmt.Println(dat)
// Access decoded values
num := dat["num"].(float64)
fmt.Println(num)
// Decode into struct
str := `{"page": 1, "fruits": ["apple", "peach"]}`
res := response2{}
json.Unmarshal([]byte(str), &res)
fmt.Println(res)
fmt.Println(res.Fruits[0])
// Stream encoding to stdout
enc := json.NewEncoder(os.Stdout)
d := map[string]int{"apple": 5, "lettuce": 7}
enc.Encode(d)
}

Key Concepts:

  • json.Marshal(): Encode Go values to JSON
  • json.Unmarshal(): Decode JSON to Go values
  • Exported fields only: Fields must start with capital letters
  • Struct tags: Customize JSON field names with json:"fieldname"
  • Streaming:
    • json.NewEncoder(): Stream encoding to io.Writer
    • json.NewDecoder(): Stream decoding from io.Reader
  • Use streaming for HTTP responses and large data

Here are other important topics from Go by Example:

Control Flow:

  • For loops (only loop construct in Go)
  • If/Else
  • Switch statements
  • Range (iterate over collections)

Functions:

  • Multiple return values
  • Variadic functions
  • Closures
  • Recursion

Data Structures:

  • Arrays (fixed size)
  • Slices (dynamic)
  • Maps (hash tables)

Pointers & Methods:

  • Pointers
  • Methods on structs
  • Interfaces
  • Struct embedding

Advanced Concurrency:

  • Channel directions (send-only, receive-only)
  • Timeouts
  • Non-blocking operations
  • Closing channels
  • Timers and tickers
  • Rate limiting
  • Atomic counters
  • Mutexes

String & Text Processing:

  • String functions
  • String formatting
  • Text templates
  • Regular expressions

Data Formats:

  • XML encoding/decoding
  • Base64 encoding
  • URL parsing

Time & Random:

  • Time operations
  • Epoch time
  • Time formatting/parsing
  • Random numbers

File System:

  • Line filters
  • File paths
  • Temporary files
  • Embed directive

Command Line:

  • Command-line arguments
  • Command-line flags
  • Subcommands
  • Environment variables

Testing & Logging:

  • Testing and benchmarking
  • Logging

Network:

  • TCP server
  • Signals

Process Management:

  • Spawning processes
  • Executing processes
  • Exit codes

  1. Practice: Work through examples at gobyexample.com
  2. Interactive Tour: Visit go.dev/tour for hands-on exercises
  3. Documentation: Read pkg.go.dev for package documentation
  4. Effective Go: Study go.dev/doc/effective_go
  5. Build Projects: Create real applications using these concepts

  • Error handling: Always check errors, don’t ignore them
  • Defer: Use immediately after acquiring resources
  • Goroutines: Lightweight, but always synchronize properly
  • Channels: Prefer communication over shared memory
  • Interfaces: Small interfaces are better (1-3 methods)
  • Formatting: Use go fmt to format code
  • Testing: Write tests with _test.go files
  • Modules: Use Go modules for dependency management

Content compiled from Go by Example and A Tour of Go