Go

Go Language Fundamentals

Go Language Fundamentals

Go (or Golang) is a statically typed, compiled language designed at Google. It's known for its simplicity, excellent performance, and first-class support for concurrency. This guide covers the fundamentals you need to start writing Go.

Hello, World!

Every Go program starts with a package declaration. The main package is special - it defines an executable program:

go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
bash
# Run the program
go run main.go

# Build an executable
go build -o myapp main.go
./myapp

Variables and Types

Go is statically typed, but type inference makes declarations concise:

go
package main

import "fmt"

func main() {
    // Explicit type declaration
    var name string = "Alice"
    var age int = 30

    // Type inference (preferred)
    count := 42
    price := 19.99
    isActive := true

    // Multiple declarations
    var x, y, z int = 1, 2, 3
    a, b := "hello", "world"

    // Constants
    const MaxItems = 100
    const (
        StatusPending = iota  // 0
        StatusActive          // 1
        StatusComplete        // 2
    )

    // Zero values (Go initializes variables to zero values)
    var i int       // 0
    var s string    // ""
    var p *int      // nil
    var slice []int // nil

    fmt.Println(name, age, count, price, isActive)
}

Functions

Go functions can return multiple values, making error handling explicit and clean:

go
package main

import (
    "errors"
    "fmt"
)

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

// Named return values (useful for documentation)
func rectangle(w, h float64) (area, perimeter float64) {
    area = w * h
    perimeter = 2 * (w + h)
    return // naked return uses named values
}

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

// Function as a value
func apply(fn func(int) int, value int) int {
    return fn(value)
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result) // 5

    area, perim := rectangle(3, 4)
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", area, perim)

    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    double := func(x int) int { return x * 2 }
    fmt.Println(apply(double, 5)) // 10
}

Structs and Methods

Go uses structs instead of classes. Methods are defined separately from struct declarations:

go
package main

import "fmt"

// Struct definition
type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

// Method with value receiver (doesn't modify the struct)
func (u User) FullName() string {
    return u.Name
}

// Method with pointer receiver (can modify the struct)
func (u *User) Deactivate() {
    u.IsActive = false
}

// Constructor function (Go convention)
func NewUser(id int, name, email string) *User {
    return &User{
        ID:       id,
        Name:     name,
        Email:    email,
        IsActive: true,
    }
}

func main() {
    // Create struct instances
    user1 := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
    user2 := NewUser(2, "Bob", "bob@example.com")

    fmt.Println(user1.FullName()) // Alice
    fmt.Println(user2.IsActive)   // true

    user2.Deactivate()
    fmt.Println(user2.IsActive)   // false
}

Interfaces

Go interfaces are implicit - any type that implements the required methods satisfies the interface:

go
package main

import "fmt"

// Interface definition
type Writer interface {
    Write(data []byte) (int, error)
}

type Reader interface {
    Read(p []byte) (int, error)
}

// Composed interface
type ReadWriter interface {
    Reader
    Writer
}

// Any type with a Write method satisfies Writer
type ConsoleWriter struct{}

func (cw ConsoleWriter) Write(data []byte) (int, error) {
    n, err := fmt.Print(string(data))
    return n, err
}

// Function accepting interface
func writeData(w Writer, data string) {
    w.Write([]byte(data))
}

func main() {
    cw := ConsoleWriter{}
    writeData(cw, "Hello, interfaces!\n")
}
💡 Go interfaces are satisfied implicitly. You don't need to declare that a type implements an interface - if it has the methods, it implements the interface.

Goroutines and Channels

Go's killer feature is goroutines - lightweight threads managed by the Go runtime. Channels provide safe communication between goroutines:

go
package main

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

// Goroutine function
func fetchURL(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("%s: error - %v", url, err)
        return
    }
    defer resp.Body.Close()

    elapsed := time.Since(start)
    ch <- fmt.Sprintf("%s: %d (%v)", url, resp.StatusCode, elapsed)
}

func main() {
    urls := []string{
        "https://www.google.com",
        "https://www.github.com",
        "https://www.golang.org",
    }

    // Buffered channel
    ch := make(chan string, len(urls))

    // Launch goroutines
    for _, url := range urls {
        go fetchURL(url, ch)  // 'go' keyword launches a goroutine
    }

    // Collect results
    for range urls {
        fmt.Println(<-ch)  // Receive from channel
    }
}
Concurrency deep dive
Goroutines are extremely lightweight (2KB initial stack vs 1MB for OS threads). You can easily spawn thousands of them.

Channels are typed conduits for sending and receiving values. They're the preferred way to communicate between goroutines.

Buffered channels (make(chan string, 10)) allow sending without an immediate receiver, up to the buffer size.

Select statement lets you wait on multiple channel operations:
go select { case msg := <-ch1: fmt.Println(msg) case ch2 <- value: fmt.Println("sent") case <-time.After(1 * time.Second): fmt.Println("timeout") }

Error Handling

Go handles errors explicitly through return values, not exceptions:

go
package main

import (
    "errors"
    "fmt"
    "os"
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// Function that returns an error
func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap errors with context
        return nil, fmt.Errorf("failed to read config: %w", err)
    }
    return data, nil
}

func main() {
    data, err := readConfig("config.json")
    if err != nil {
        // Check for specific error types
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Config file not found, using defaults")
        } else {
            fmt.Printf("Error: %v\n", err)
            os.Exit(1)
        }
    }
    fmt.Printf("Config loaded: %d bytes\n", len(data))
}

Key Takeaways

  1. Go is simple by design - there's usually one obvious way to do things
  2. Use := for short variable declarations within functions
  3. Functions can return multiple values; use this for error handling
  4. Interfaces are satisfied implicitly - very powerful for testing
  5. Goroutines + channels = safe, easy concurrency
  6. Error handling is explicit - check errors, don't ignore them
  7. Use 'go fmt' to format code - all Go code looks the same
  8. The standard library is excellent - explore it before reaching for packages

Related Posts

Comments

Log in to leave a comment.

Log In

Loading comments...