GOLANG

A crash course for the GoLang programming language.
The fundamentals quick + easy.


GO.000

What makes Go stand out from other programming languages?

Its simplicity, efficiency, and built-in concurrency make it perfectly suited for optimizing programs on modern computing environments.

Designed by Google in 2007, Go was created specifically to scale! (aka perfect for parallel programming).

It has the performance comparable to C but has the benefit of more human-readable syntax.


(also the cutest logo ^_^)

Go: The Essentials

Statically typed

- but with type inference to reduce verbosity

Garbage collected

- for memory management without manual intervention

Compiled

- to machine code for efficiency

Built-in concurrency

- through goroutines and channels

Fast compilation

- for improved developer productivity

Standard formatting

- enforced by tools like gofmt
[ 1.0 INTRO ]

Jump to Topic

GO.001

Setting Up Go

Installing Go is straightforward across platforms:


    # Check Go version after installation
                            
        $ go version
        > go version go1.18.3 darwin/amd64
                            
    # Create a new module
                            
        $ mkdir hello
        $ cd hello
        $ go mod init example.com/hello
        $ go run .
                    
[ 1.0 INTRO ]
GO.002

Basic Syntax


        package main

        import (
            "fmt"
            "time"
        )
                    
        // This is a comment
        
        func main() {
            fmt.Println("Hello, Go!")

            // Variables
            var age int = 30
            name := "Alice"  // Short declaration

            // Constants
            const MaxConnections = 100

            // Control flow
            if age > 18 {
                fmt.Println(name, "is an adult")
            }

            // Loops
            for i := 0; i < 5; i++ {
                fmt.Println(i)
            }

            // While-style loop
            count := 0
            for count < 5 {
                count++
            }
        }
            
Feature Description
Goroutines Lightweight threads managed by the Go runtime
Channels Typed conduits for sending and receiving values
Interfaces Implicit implementation for flexibility
Defer Execution of functions at the end of the current scope
Error handling Explicit error values rather than exceptions

Data Types & Structures

Basic Types

                    
        // Numeric types
        var i int = 42
        var f float64 = 3.14
        var c complex128 = 1 + 2i

        // String and boolean
        var s string = "hello"
        var b bool = true

        // Zero values
        var z int      // zero value: 0
        var z2 string  // zero value: ""
        var z3 bool    // zero value: false
                

Composite Types

                    
        // Arrays - fixed size
        var arr [5]int = [5]int{1, 2, 3, 4, 5}

        // or with type inference
        arr2 := [3]string{"a", "b", "c"}

        // Slices - dynamic size
        slice := []int{1, 2, 3}
        slice = append(slice, 4, 5)

        // Maps - key-value pairs
        m := make(map[string]int)
        m["one"] = 1
        m["two"] = 2

        // Checking for existence
        value, exists := m["three"]
        if !exists {
            fmt.Println("Key does not exist")
        }

        // Structs - composite types
        type Person struct {
            Name string
            Age  int
        }

        p := Person{Name: "Alice", Age: 30}
        fmt.Println(p.Name) // Access field with dot notation
            

Functions & Methods


        // Basic function
        func add(a, b int) int {
            return a + b
        }

        // Multiple return values
        func divide(a, b float64) (float64, error) {
            if b == 0 {
                return 0, errors.New("division by zero")
            }
            return a / b, nil
        }

        // Variadic function
        func sum(nums ...int) int {
            total := 0
            for _, num := range nums {
                total += num
            }
            return total
        }

        // Methods (functions attached to types)
        type Rectangle struct {
            Width, Height float64
        }

        // Method with receiver
        func (r Rectangle) Area() float64 {
            return r.Width * r.Height
        }

        // Method with pointer receiver (can modify the receiver)
        func (r *Rectangle) Scale(factor float64) {
            r.Width *= factor
            r.Height *= factor
        }
            

Control Flow

Conditional Statements


            // If statement
            if x > 10 {
                fmt.Println("x is greater than 10")
            } else if x < 0 {
                fmt.Println("x is negative")
            } else {
                fmt.Println("x is between 0 and 10")
            }

            // If with short statement
            if err := doSomething(); err != nil {
                fmt.Println("Error:", err)
            }

            // Switch statement
            switch os := runtime.GOOS; os {
            case "darwin":
                fmt.Println("OS X.")
            case "linux":
                fmt.Println("Linux.")
            default:
                fmt.Printf("%s.\n", os)
            }

            // Switch without expression (like if-else)
            switch {
            case t.Hour() < 12:
                fmt.Println("Good morning!")
            case t.Hour() < 17:
                fmt.Println("Good afternoon!")
            default:
                fmt.Println("Good evening!")
            }
            

Loops


            // Basic for loop
            for i := 0; i < 10; i++ {
                fmt.Println(i)
            }

            // For as a while loop
            i := 0
            for i < 10 {
                i++
            }

            // Infinite loop with break
            for {
                // Do something repeatedly
                if condition {
                    break // Exit the loop
                }
                if otherCondition {
                    continue // Skip to the next iteration
                }
            }

            // Range loop for slices, arrays, maps, strings
            names := []string{"Alice", "Bob", "Charlie"}
            for i, name := range names {
                fmt.Printf("%d: %s\n", i, name)
            }

            // Range with maps
            for key, value := range m {
                fmt.Printf("%s: %d\n", key, value)
            }
                

Error Handling

Go uses explicit error values rather than exceptions:


            // Function returning an error
            func OpenFile(name string) (*File, error) {
                if fileNotExist(name) {
                    return nil, errors.New("file does not exist")
                }
                // Open the file and return it
                return file, nil
            }

            // Using a function that may return an error
            f, err := OpenFile("data.txt")
            if err != nil {
                // Handle error
                fmt.Println("Error:", err)
                return
            }

            // Use the file
            defer f.Close() // Called when surrounding function returns
                

Creating Custom Errors


            // Custom error type
            type MyError struct {
                Code    int
                Message string
            }

            // Implement the error interface
            func (e *MyError) Error() string {
                return fmt.Sprintf("%d: %s", e.Code, e.Message)
            }

            // Return custom error
            func doSomething() error {
                return &MyError{
                    Code:    500,
                    Message: "something went wrong",
                }
            }
                
[ 2.0 BASICS ]
GO.003

Concurrency with Goroutines

Go's lightweight threads, called goroutines, enable concurrent programming patterns with minimal overhead:

                    
        func main() {

            // Start a goroutine
            go sayHello("world")

            // Main goroutine continues execution
            fmt.Println("Hello from main")

            // Wait for goroutines to finish
            time.Sleep(100 * time.Millisecond)

        }

        func sayHello(name string) {
            fmt.Println("Hello,", name)
        }
        
GO CONCURRENCY G1 G2 G3 G4 CHANNEL MAIN GOROUTINES BUFFERED CHANNEL

WaitGroups for Synchronization


        func main() {
            var wg sync.WaitGroup

            // Launch 5 goroutines
            for i := 0; i < 5; i++ {
                wg.Add(1) // Increment counter
                go func(id int) {
                    defer wg.Done() // Decrement counter when done
                    fmt.Printf("Worker %d done\n", id)
                }(i)
            }

            // Wait for all goroutines to finish
            wg.Wait()
            fmt.Println("All workers done")
        }

Channels

Channels provide a mechanism for goroutines to communicate and synchronize:


    func main() {
        // Create a channel
        ch := make(chan string)

        // Send data on channel in a goroutine
        go func() {
            ch <- "Hello from goroutine"
        }()

        // Receive from channel (blocks until data is available)
        msg := <-ch
        fmt.Println(msg)

        // Buffered channels
        buffer := make(chan int, 3) // Can hold 3 ints
        buffer <- 1
        buffer <- 2
        buffer <- 3
        // Now buffer is full, next send would block

        fmt.Println(<-buffer) // 1
        fmt.Println(<-buffer) // 2
        fmt.Println(<-buffer) // 3
    }

Selecting from Multiple Channels


    func main() {

        ch1 := make(chan string)
        ch2 := make(chan string)

        // Send on ch1 after 1 second
        go func() {
            time.Sleep(1 * time.Second)
            ch1 <- "one"
        }()

        // Send on ch2 after 2 seconds
        go func() {
            time.Sleep(2 * time.Second)
            ch2 <- "two"
        }()

        // Select blocks until one of its cases can proceed
        for i := 0; i < 2; i++ {
            select {
            case msg1 := <-ch1:
                fmt.Println("Received", msg1)
            case msg2 := <-ch2:
                fmt.Println("Received", msg2)
            case <-time.After(3 * time.Second):
                fmt.Println("Timeout")
                return
            }
        }
    }

Concurrency Patterns: Fan-Out, Fan-In

Distribute work across multiple goroutines and collect the results:


    func main() {

        // Generate work
        jobs := make(chan int, 100)
        results := make(chan int, 100)

        // Start 3 workers (fan-out)
        for w := 1; w <= 3; w++ {
            go worker(w, jobs, results)
        }

        // Send jobs
        for j := 1; j <= 9; j++ {
            jobs <- j
        }
        close(jobs)

        // Collect results (fan-in)
        for a := 1; a <= 9; a++ {
            <-results
        }
    }

    func worker(id int, jobs <-chan int, results chan<- int) {
        for j := range jobs {
            fmt.Printf("Worker %d processing job %d\n", id, j)
            time.Sleep(time.Second)
            results <- j * 2
        }
    }

[ 3.0 CONCURRENCY ]
GO.004

Interfaces

Interfaces define behavior without prescribing implementation:


        // Interface definition
        type Reader interface {
            Read(p []byte) (n int, err error)
        }

        type Writer interface {
            Write(p []byte) (n int, err error)
        }

        // Composition of interfaces
        type ReadWriter interface {
            Reader
            Writer
        }

        // Implementing an interface
        type File struct {
            // ...
        }

        // File implements Reader interface
        func (f *File) Read(p []byte) (n int, err error) {
            // Implementation here
            return len(p), nil
        }

        // File implements Writer interface
        func (f *File) Write(p []byte) (n int, err error) {
            // Implementation here
            return len(p), nil
        }

        // Using an interface
        func Copy(dst Writer, src Reader) error {
            // Any type that implements these interfaces can be used
            // ...
        }

The Empty Interface


        // Empty interface can hold any value
        var i interface{}
        i = 42
        i = "hello"
        i = struct{ Name string }{"Alice"}

        // Type assertions
        s, ok := i.(string)
        if !ok {
            fmt.Println("i is not a string")
        }

        // Type switches
        switch v := i.(type) {
        case int:
            fmt.Println("i is an int:", v)
        case string:
            fmt.Println("i is a string:", v)
        default:
            fmt.Println("unknown type")
        }

Project Structure & Modules

Go code is organized into packages and modules:

Packages


        // File: math/calculation.go
        package math

        // Exported function (starts with uppercase)
        func Add(a, b int) int {
            return a + b
        }

        // Unexported function (starts with lowercase)
        func multiply(a, b int) int {
            return a * b
        }


        

Importing Packages


        package main

        import (
            "fmt"
            "example.com/myproject/math"
        )

        func main() {
            result := math.Add(5, 3)
            fmt.Println(result) // 8

            // math.multiply(5, 3) // Error: unexported
        }

Go Modules


        // Initialize a new module
        $ go mod init example.com/myproject

        // Add a dependency
        $ go get github.com/pkg/errors

        // Update dependencies
        $ go get -u

        // Tidy dependencies (add missing, remove unused)
        $ go mod tidy

Testing

Go includes built-in support for testing:

Writing Tests


        // File: math/calculation_test.go
        package math

        import "testing"

        func TestAdd(t *testing.T) {
            got := Add(2, 3)
            want := 5

            if got != want {
                t.Errorf("Add(2, 3) = %d; want %d", got, want)
            }
        }

        // Table-driven tests
        func TestMultiply(t *testing.T) {
            tests := []struct {
                a, b, want int
            }{
                {2, 3, 6},
                {-1, 5, -5},
                {0, 10, 0},
            }

            for _, tc := range tests {
                got := multiply(tc.a, tc.b)
                if got != tc.want {
                    t.Errorf("multiply(%d, %d) = %d; want %d",
                             tc.a, tc.b, got, tc.want)
                }
            }
        }

Running Tests


        // Run all tests
        $ go test ./...

        // Run tests with verbose output
        $ go test -v

        // Run a specific test
        $ go test -run TestAdd

        // Run benchmark tests
        $ go test -bench=.

        // Check test coverage
        $ go test -cover

Standard Library Highlights

Go has a rich standard library that covers most common needs:

  • fmt - Formatted I/O with functions like Printf
  • io - Basic I/O interfaces
  • os - Platform-independent OS functionality
  • strings - String manipulation functions
  • time - Time and duration functionality
  • net/http - HTTP client and server implementations
  • encoding/json - JSON encoding and decoding
  • sync - Synchronization primitives
  • context - Package for managing deadlines, cancellations, etc.

HTTP Server Example


        package main

        import (
            "fmt"
            "log"
            "net/http"
        )

        func handler(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
        }

        func main() {
            http.HandleFunc("/", handler)
            log.Println("Server starting on port 8080...")
            log.Fatal(http.ListenAndServe(":8080", nil))
        }

[ 4.0 PACKAGES ]
GO.005

Best Practices

Error Handling

  • Check errors immediately after function calls that return them
  • Wrap errors with context using packages like github.com/pkg/errors
  • Don't use panic for regular error handling

Code Organization

  • Group related code into packages based on functionality
  • Follow Go's standard project layout where appropriate
  • Avoid deeply nested package hierarchies

Naming Conventions

  • Package names: short, concise nouns (e.g., time, http)
  • Function and variable names: camelCase for unexported, PascalCase for exported
  • Interface names: often end with -er (e.g., Reader, Writer)
  • Use meaningful but concise names; abbreviate judiciously

Performance Tips

Memory Management

  • Preallocate slices and maps when you know the approximate size
  • Reuse memory with sync.Pool for frequently allocated objects
  • Be aware of heap allocations vs. stack allocations

Concurrency

  • Don't overuse goroutines; they're lightweight but not free
  • Use buffered channels appropriately
  • Profile and benchmark to identify bottlenecks
  • Consider the context package for timeouts and cancellation

        // Example of a context with timeout
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel() // Always call cancel to release resources

        // Use ctx in HTTP requests, database calls, etc.
        req, err := http.NewRequestWithContext(ctx, "GET", url, nil)

GO.005

Tools & Ecosystem

Essential Tools

  • go fmt - Format code according to Go standards
  • go vet - Find potential issues in code
  • go test - Run tests and benchmarks
  • go build - Compile packages and dependencies
  • go run - Compile and run a Go program
  • go mod - Module maintenance

Popular Packages

  • Web frameworks: Gin, Echo, Fiber
  • Database: GORM, sqlx, pgx
  • CLI apps: Cobra, Viper
  • Testing: Testify, GoMock
  • Logging: Zap, Logrus
  • Configuration: Viper

Conclusion

Go excels at building reliable, efficient systems with minimal complexity. Its approach to concurrency through goroutines and channels enables developers to write highly concurrent code without the traditional challenges of thread management.

The language's focus on simplicity, consistency, and practicality has made it popular for a wide range of applications, from cloud services and CLI tools to web applications and microservices.

By embracing Go's idioms and best practices, you'll find that the language encourages a style of programming that is both productive and produces robust, maintainable code.

Further Learning Resources

[ 5.0 PRACTICES ]
Back to Top