andersch.dev

<2024-11-01 Fri>

Go (Golang)

Go (Golang) is a statically-typed, compiled programming language by Google that has strong support for concurrent programming. Its standard library (with features such as built-in HTTP server and JSON support) makes it a suitable backend language for scalable and high-performance applications.

Go's concurrency model (using Goroutines and Channels) can handle multiple tasks simultaneously with minimal complexity.

Keywords

package / import

The first line of every .go file specifies the package name.

package main // declare main package (must contain main() function)

/* packages can be imported */
import "package"
import (
    "fmt"
    "time"
    _ "embed" // mark package as unused
    myname "github.com/path/to/library"
)

// private/public identifiers via capitalization
var foo := 3 // private to package
var Bar := 3 // will be exported
func DoThing() { /* ... */ } // can be called outside package

for, switch, range (Control flow)

/* switch */
switch state {
    case 1:
        /* ... */
        goto label;// jump to label
    case 2:
        /* ... */
        fallthrough
    case 3:
        label:  // goto label declaration
        /* ... */
        break // always implicitly there
    default:
}

/* loops */
for count < 5 {
    if count == 3 {
        continue
    }
}
for { /* ... */ } // like a while(true) loop

/* ranges */
scores := map[string]int{"Alice": 90, "Bob":   85}
for name, score := range scores {
    /* ... */
}

Types

/* types */
var         // declare a variable
const       // declare a constant
map[string] // declare map type (hashtable/dictionary)
type Person struct {  // define struct type
    name string
    age  int
}

interface{} // any type
any         // alias for interface{}

/* enums in go: use "iota" to declare constants */
type Weekday int
const (
    _                 = iota   // ignore first value (iota == 0)
    MONDAY    Weekday = iota   // == 1
    TUESDAY                    // == 2
    WEDNESDAY
    THURSDAY
    FRIDAY
    SATURDAY
    SUNDAY
)

Functions/Interfaces

// functions
func example(var a int) (int, error) {
    defer fmt.Printf("Finished\n") // defer function call to end of function
    return a, nil
}

/* interfaces */
type Shape interface  { // define interface named Shape
    Area() float64
}
type Circle struct {
    radius float64
}
func (c Circle) Area() float64 { // implement interface for Circle
    return 3.14 * c.radius * c.radius
}

go, chan, select (Concurrency)

/* goroutines/channels */
go      // start a goroutine
chan    // declare channel type
select  // wait on multiple channel operations

Concepts

goroutine

A goroutine is a lightweight thread managed by the Go runtime.

package main

import (
    "fmt"
    "time"
)

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    go greet("Alice")           // start goroutine
    go greet("Bob")             // start another
    time.Sleep(1 * time.Second) // wait for goroutines to finish
    fmt.Println("Finished.")
}

Using WaitGroup to wait for goroutine

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // notify that the goroutine is done
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(2 * time.Second) // simulate work
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)         // increment WaitGroup counter
        go worker(i, &wg) // start goroutine
    }

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

chan (Channels)

Channels enable communication between goroutines.

package main
import "fmt"

func calculateSquare(num int, ch chan int) {
    ch <- num * num // send result to channel
}

func main() {
    ch := make(chan int) // create channel

    for i := 1; i <= 5; i++ {
        go calculateSquare(i, ch) // start goroutine
    }

    for i := 1; i <= 5; i++ {
        fmt.Println("Square:", <-ch) // receive result from the channel
    }
}

Channels can be directional:

var ch chan int       // bidirectional (send & receive)
var sendCh chan<- int // send-only channel
var recvCh <-chan int // receive-only channel

Channels can be buffered or unbuffered:

  • Buffered for synchronous, blocking communication
  • Unbuffered for asynchronous, non-blocking communication
  • Unbuffered better for coordination, buffered better for throughput
  • Buffered can mask deadlocks that unbuffered would reveal
// Unbuffered channel
ch1 := make(chan int)
// - Synchronous communication
// - Sender blocks until receiver takes value
// - Receiver blocks until sender provides value
// - Provides guaranteed delivery and synchronization point

// Buffered channel
ch2 := make(chan int, 5) // Buffered with capacity 5
// - Asynchronous until buffer fills
// - Sender only blocks when buffer is full
// - Receiver blocks only when buffer is empty
// - Decouples sender and receiver timing

If no more messages need to be sent over the channel, it can be closed:

// sending side
close(ch) // close channel

val, ok := <-ch
if !ok {
        // channel has closed (buffered channels stay open until emptied)
}

select

The select statement enables concurrent operations on channels.

Characteristics:

  • Blocks until a channel operation can proceed
  • Randomly selects if multiple cases are ready
  • Can include non-blocking default case
  • Often used in goroutines for concurrent operations
  • Can be combined with timeouts using time.After()
select {
case <-ch1:
    // Execute if data received from ch1
case x := <-ch2:
    // Execute if data received from ch2, store in x
case ch3 <- value:
    // Execute if data can be sent to ch3
case <-time.After(1 * time.Second):
    fmt.Println("Timed out")
default:
    // Execute if no other case is ready (optional)
}

Module System

go.mod: The go.mod is the config file for a Go module.

  • Declares module path (module mymodule).
  • Declares required go version (go 1.21.1)
  • Lists module's dependencies (and versions) with require (...)
  • Can include replace directives (useful for testing)
  • Can also exclude modules

go.sum:

  • The go.sum file is used to manage dependencies.
  • It contains the url for the module, its version and a cryptographic checksum.
  • It is generated and updated when running go get, go mod tidy, or go build.

Libraries

net/http

The standard library net/http provides HTTP client/server implementations. It can be used to build web apps, APIs, and handling HTTP requests/responses.

Hello World Server example

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    //http.Handle(pattern string, handler Handler)

    //http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    http.HandleFunc("/", handler)

    //http.ListenAndServe(addr string, handler Handler) error
    http.ListenAndServe(":8080", nil)
}

html/template

Go provides templating utilities for text with text/template. For templating HTML, html/template should be used to ensure special characters are rendered as plaintext (preventing Cross-Site Scripting (XSS) attacks).

package main

import (
    "html/template"
    "net/http"
)

// Data structure to hold the template data
type Data struct {
    Name  string
    Count int
}

// HTML template string
const htmlTmplStr = `<h1>Hello, {{.Name}}!</h1><p>You have {{.Count}} new messages.</p>`

// HTTP handler function
func handler(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.New("greeting").Parse(htmlTmplStr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    data := Data{Name: "Alice", Count: 5}
    err = tmpl.Execute(w, data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

database/sql

package main

import (
        "database/sql"
        "fmt"
        "log"
        "time"

        _ "github.com/lib/pq" // PostgreSQL driver
)

type User struct {
        ID        int
        Email     string
        CreatedAt time.Time
}

func main() {
        // Connect to database
        connStr := "postgres://user:password@localhost/dbname?sslmode=disable"
        db, _ := sql.Open("postgres", connStr)
        defer db.Close()

        // create table
        db.Exec(`
                CREATE TABLE IF NOT EXISTS users (
                        id SERIAL PRIMARY KEY,
                        email TEXT NOT NULL UNIQUE,
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
        `)

        // inserting data
        email := "test@example.com"; var id int
        err = db.QueryRow("INSERT INTO users (email) VALUES ($1) RETURNING id", email,).Scan(&id)
        fmt.Printf("Inserted user with ID: %d\n", id)

        // Query single row
        var user User
        err = db.QueryRow("SELECT id, email, created_at FROM users WHERE id = $1", id).Scan(&user.ID, &user.Email, &user.CreatedAt)
        fmt.Printf("User: %+v\n", user)

        // Query multiple rows
        rows, err := db.Query("SELECT id, email, created_at FROM users")
        defer rows.Close()

        fmt.Println("\nAll users:")
        for rows.Next() {
                var u User
                if err := rows.Scan(&u.ID, &u.Email, &u.CreatedAt); err != nil {
                        log.Fatal(err)
                }
                fmt.Printf("%+v\n", u)
        }

        // use prepared statement
        stmt, _ := db.Prepare("UPDATE users SET email = $1 WHERE id = $2")
        defer stmt.Close()

        res, _ := stmt.Exec("updated@example.com", id)
        rowsAffected, _ := res.RowsAffected()
        fmt.Printf("\nUpdated %d rows\n", rowsAffected)

        // Transaction example
        tx, err := db.Begin()

        _, err = tx.Exec("INSERT INTO users (email) VALUES ($1)", "transaction@example.com")
        if err != nil { tx.Rollback() }

        err = tx.Commit()
}

Idioms

Struct Embedding

Add types without a name to a struct to embed all of its members directly. The type can then call methods of that embedded type directly.

import "sync"

type Message struct {
    sync.RWMutex
    text string
}

func main() {
        msg := Message{}
        msg.Lock(); defer msg.Unlock();
}

Shorthand for Error Handling

if err := writer.Close(); err != nil {
        panic(err)
}

Interface Satisfaction Check

Ensure a type implements interfaces at compile time:

var _ io.Reader = (*MyType)(nil)

If MyType doesn't implement Read(p []byte) (n int, err error), the compiler will throw an error.