Skip to content

marac19901990/golang-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

Golang Learning Notes

Fundamental Language Characteristics

Strongly Typed Language

Go is a strongly typed language, which means:

  • Type safety is enforced: Once a variable is declared with a specific type, you cannot assign a value of a different type to it without explicit conversion
  • No implicit type coercion: Go will not automatically convert between types (e.g., you can't add an integer to a string without converting one of them first)
  • Catches type errors at compile time: Most type-related errors are caught before your program runs, preventing many common bugs

Example:

var age int = 25
var name string = "Marko"

// This will cause a compile error:
// age = name  // cannot use name (type string) as type int

// You must explicitly convert:
// age = int(name)  // This would still fail because you can't convert string "Marko" to int

Benefits:

  • Fewer runtime errors
  • Better code reliability
  • Easier to refactor code with confidence
  • IDE and tooling can provide better autocomplete and error detection

Statically Typed Language

Go is a statically typed language, which means:

  • Types are determined at compile time: Variable types must be known when the code is compiled, not when it runs
  • Type checking happens before execution: The compiler verifies all type information before generating the executable
  • Explicit or inferred type declarations: You can declare types explicitly (var x int = 5) or let the compiler infer them (x := 5)

Example:

// Explicit type declaration
var count int = 10

// Type inference (compiler determines type from the value)
message := "Hello, Go!"  // message is inferred as string
isActive := true         // isActive is inferred as bool

// Once declared, the type cannot change
count = 20        // OK - still an int
// count = "20"   // ERROR - cannot assign string to int variable

Note: Static typing (Go, Java, C++) checks types at compile time, while dynamic typing (Python, JavaScript, PHP) checks at runtime.


Cross-Platform

Go is cross-platform, which means:

  • Write once, compile anywhere: The same Go source code can be compiled to run on different operating systems and architectures
  • Native support for multiple targets: Go can compile binaries for Windows, Linux, macOS, BSD, and more
  • Easy cross-compilation: You can build executables for different platforms from a single development machine

Example - Cross-compilation:

# On Linux, compile for Windows
GOOS=windows GOARCH=amd64 go build -o myapp.exe

# On macOS, compile for Linux
GOOS=linux GOARCH=amd64 go build -o myapp

# On any platform, compile for ARM (e.g., Raspberry Pi)
GOOS=linux GOARCH=arm go build -o myapp-arm

Supported platforms include:

  • Operating Systems: Linux, Windows, macOS, FreeBSD, OpenBSD, Plan 9, and more
  • Architectures: amd64 (x86-64), 386 (x86), arm, arm64, and others

Benefits:

  • Deploy the same codebase to different environments
  • No need to maintain platform-specific code (in most cases)
  • Single binary with no dependencies makes deployment simple

Compiled Language

Go is a compiled language, which means:

  • Source code is translated to machine code: The Go compiler converts your .go files directly into executable binaries
  • No interpreter needed: The resulting binary runs directly on the target machine without requiring Go to be installed
  • Fast execution: Compiled code runs much faster than interpreted languages because it's already in machine code
  • Standalone binaries: Go produces single, self-contained executable files with no external dependencies (by default)

Compilation process:

# Compile a Go program
go build main.go

# This creates an executable binary (e.g., 'main' or 'main.exe')
# You can run it directly:
./main  # On Linux/macOS
main.exe  # On Windows

Note: Compiled languages (Go, C, Rust) translate to machine code before execution, while interpreted languages (Python, JavaScript, PHP) execute code line by line at runtime.

Go's compilation benefits:

  • Very fast compilation: Despite being compiled, Go compiles incredibly quickly (feels almost like an interpreted language during development)
  • Static linking by default: All dependencies are included in the binary (no "dependency hell")
  • Easy deployment: Just copy the single binary file to your server
  • Performance: Execution speed comparable to C/C++

Example workflow:

// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
}
# Compile
go build main.go

# Run the compiled binary
./main
# Output: Hello, Go!

Visibility Through Naming Convention

Go uses a unique approach to visibility (public/private access control) based on the capitalization of the first letter of identifiers:

  • Capitalized (starts with uppercase letter) → Exported (public, visible outside the package)
  • Lowercase (starts with lowercase letter) → Unexported (private, only visible within the package)

This applies to:

  • Functions
  • Variables
  • Constants
  • Types (structs, interfaces)
  • Struct fields
  • Methods

Example:

package mypackage

// Exported (Public) - can be used by other packages
func ProcessData() {
    // ...
}

var MaxConnections int = 100

type User struct {
    Name  string  // Exported field - accessible from other packages
    Email string  // Exported field
    age   int     // Unexported field - only accessible within mypackage
}

// Unexported (Private) - only usable within mypackage
func validateEmail(email string) bool {
    // ...
    return true
}

var internalCounter int = 0

type config struct {
    apiKey string
    timeout int
}

Using the exported items from another package:

package main

import "myproject/mypackage"

func main() {
    // ✓ Can use exported function
    mypackage.ProcessData()
    
    // ✓ Can access exported variable
    maxConn := mypackage.MaxConnections
    
    // ✓ Can create exported type
    user := mypackage.User{
        Name:  "Marko",
        Email: "marko@example.com",
        // age: 30,  // ✗ ERROR - cannot access unexported field
    }
    
    // ✓ Can access exported fields
    println(user.Name)
    
    // ✗ ERROR - cannot use unexported function
    // mypackage.validateEmail("test@example.com")
    
    // ✗ ERROR - cannot access unexported variable
    // mypackage.internalCounter
}

Key Points:

  1. Package-level visibility: Unlike many languages (Java, C++, PHP), Go doesn't have public, private, or protected keywords. Visibility is controlled purely by naming.

  2. No class-level private: In Go, there are no classes. Unexported items are private to the package, not to a specific type or struct.

  3. Enforced by the compiler: The Go compiler will produce errors if you try to access unexported identifiers from outside their package.

  4. Package = directory: A package consists of all .go files in the same directory. All files in that directory can access each other's unexported items.

Note: Unlike languages with public, private, protected keywords (PHP, Java, C++), Go uses capitalization for visibility control.

Benefits of Go's approach:

  • Simple and consistent: One rule to remember - capital = public, lowercase = private
  • No keyword clutter: Cleaner, more readable code
  • Visible at a glance: You can immediately see what's public just by looking at the name
  • Package-based encapsulation: Encourages good package design and clear boundaries

Best practices:

  • Export only what needs to be used by other packages (minimize public API surface)
  • Use descriptive names for exported items since they form your package's public interface
  • Keep implementation details unexported to allow internal refactoring without breaking consumers

Defer Statement

Go's defer statement schedules a function call to be executed after the surrounding function returns, regardless of how it returns (normal return, early return, or panic).

Key characteristics:

  • Always executes: Deferred functions run even if the function returns early or panics
  • LIFO order: Multiple defers execute in Last-In-First-Out (stack) order
  • Arguments evaluated immediately: Function arguments are evaluated when defer is called, not when it executes

Basic usage:

func processFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()  // Ensures file is closed when function exits
    
    // Do work with file...
    data, err := io.ReadAll(file)
    if err != nil {
        return err  // file.Close() still called before return
    }
    
    // More processing...
    return nil  // file.Close() called here too
}

Handles early returns:

func validateAndProcess(id int) error {
    defer fmt.Println("Cleanup completed")
    
    if id < 0 {
        return errors.New("invalid id")  // defer still executes
    }
    
    if id > 1000 {
        return errors.New("id too large")  // defer still executes
    }
    
    // Normal processing...
    return nil  // defer executes here too
}

Handles panics:

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
        fmt.Println("Cleanup always runs")
    }()
    
    // This will panic
    panic("something went wrong")
    
    // This line never executes, but defer still runs
    fmt.Println("This won't print")
}

Multiple defers (LIFO order):

func demonstrateLIFO() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")
    
    fmt.Println("Function body")
}

// Output:
// Function body
// Third defer
// Second defer
// First defer

Common use cases:

  1. Resource cleanup:
func databaseOperation() error {
    db, err := sql.Open("postgres", connString)
    if err != nil {
        return err
    }
    defer db.Close()
    
    // Use database...
    return nil
}
  1. Unlocking mutexes:
func updateCounter() {
    mu.Lock()
    defer mu.Unlock()
    
    counter++
    // Mutex automatically unlocked even if panic occurs
}
  1. Timing functions:
func expensiveOperation() {
    start := time.Now()
    defer func() {
        fmt.Printf("Took %v\n", time.Since(start))
    }()
    
    // Do work...
}
  1. Transaction handling:
func transferMoney() error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    
    // Perform operations...
    return nil
}

Important notes:

  • Deferred function arguments are evaluated immediately when defer is called
  • Defer has a small performance cost, but it's usually negligible
  • Defer runs before return values are passed to caller (can modify named return values)

Defer in loops - CRITICAL WARNING:

Defer inside a loop defers the execution for all iterations, not per iteration. All deferred calls accumulate and execute when the function returns.

// PROBLEMATIC - defers accumulate
func processFiles(filenames []string) {
    for _, filename := range filenames {
        file, err := os.Open(filename)
        if err != nil {
            continue
        }
        defer file.Close()  // ALL files stay open until function returns!
        
        // Process file...
    }
    // Only here do ALL defers execute
}

Problem: With 1000 files, all 1000 will remain open until the function exits, potentially causing resource exhaustion.

Solution 1 - Use anonymous function:

func processFiles(filenames []string) {
    for _, filename := range filenames {
        func() {
            file, err := os.Open(filename)
            if err != nil {
                return
            }
            defer file.Close()  // Executes at end of THIS iteration
            
            // Process file...
        }()
    }
}

Solution 2 - Explicit cleanup:

func processFiles(filenames []string) {
    for _, filename := range filenames {
        file, err := os.Open(filename)
        if err != nil {
            continue
        }
        
        // Process file...
        
        file.Close()  // Clean up immediately
    }
}

Solution 3 - Extract to separate function:

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()  // Executes when processFile returns
    
    // Process file...
    return nil
}

func processFiles(filenames []string) {
    for _, filename := range filenames {
        processFile(filename)  // Defer executes per file
    }
}

Note: Similar to finally blocks in languages like Java and PHP, but more flexible and integrated into the language.


Runes - Lower Level Than Strings

Go has runes, which represent individual Unicode code points and are more fundamental than strings.

Key concepts:

  • Rune type: rune is an alias for int32 and represents a single Unicode character
  • Strings are sequences of bytes: A string in Go is a read-only slice of bytes (UTF-8 encoded)
  • Runes handle Unicode properly: Unlike byte indexing, runes correctly handle multi-byte Unicode characters

Basic usage:

var char rune = 'A'        // Single quotes for rune literals
var emoji rune = '😀'
var cyrillic rune = 'Ж'

fmt.Printf("%c\n", char)   // Prints: A
fmt.Printf("%d\n", char)   // Prints: 65 (Unicode code point)

Strings vs Bytes vs Runes:

s := "Hello, 世界"

// Length in bytes
fmt.Println(len(s))              // 13 bytes (not 9 characters!)

// Iterating as bytes (wrong for Unicode)
for i := 0; i < len(s); i++ {
    fmt.Printf("%c ", s[i])      // Breaks on multi-byte characters
}

// Iterating as runes (correct for Unicode)
for i, r := range s {
    fmt.Printf("Index: %d, Rune: %c, Value: %d\n", i, r, r)
}

// Converting to rune slice
runes := []rune(s)
fmt.Println(len(runes))          // 9 characters
fmt.Printf("%c\n", runes[7])     // 世

Why runes matter:

text := "café"

// Byte length vs character count
fmt.Println(len(text))                    // 5 bytes (é is 2 bytes in UTF-8)
fmt.Println(len([]rune(text)))            // 4 characters

// Indexing strings gives bytes, not characters
fmt.Printf("%c\n", text[3])               // � (invalid, mid-character)

// Convert to runes for proper character access
runes := []rune(text)
fmt.Printf("%c\n", runes[3])              // é (correct)

Common operations:

// String to runes
str := "Hello"
runeSlice := []rune(str)

// Runes to string
runes := []rune{'H', 'e', 'l', 'l', 'o'}
str = string(runes)

// Single rune to string
r := 'A'
str = string(r)

// Check if character is in range
var r rune = 'Ж'
if r >= 'А' && r <= 'я' {
    fmt.Println("Cyrillic character")
}

Practical examples:

// Reverse a Unicode string correctly
func reverseString(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// Count actual characters (not bytes)
func countChars(s string) int {
    return len([]rune(s))
}

// Get substring by character position
func substring(s string, start, length int) string {
    runes := []rune(s)
    if start >= len(runes) {
        return ""
    }
    end := start + length
    if end > len(runes) {
        end = len(runes)
    }
    return string(runes[start:end])
}

Important notes:

  • Strings are immutable in Go (cannot modify individual bytes)
  • Rune conversion creates a new slice (has memory cost)
  • Use range over strings to iterate by runes automatically
  • For ASCII-only strings, bytes and runes are equivalent
  • For international text, always think in runes, not bytes

Note: Unlike languages where strings are character arrays, Go strings are byte slices requiring rune conversion for proper Unicode handling.


Composition Over OOP

Go is not an object-oriented programming (OOP) language - instead, it uses composition to build complex types and behaviors.

What Go doesn't have:

  • No classes
  • No inheritance
  • No class hierarchies
  • No constructors (uses factory functions instead)
  • No extends or implements keywords

What Go uses instead:

  • Structs for data structures
  • Interfaces for polymorphism
  • Embedding for composition
  • Methods on types (not just structs)

Composition through struct embedding:

// Instead of inheritance, embed types
type Engine struct {
    Horsepower int
    Type       string
}

func (e Engine) Start() {
    fmt.Println("Engine starting...")
}

type Car struct {
    Engine  // Embedded - Car "has-a" Engine, not "is-a" Engine
    Brand   string
    Model   string
}

// Car automatically gets Engine's methods
car := Car{
    Engine: Engine{Horsepower: 200, Type: "V6"},
    Brand:  "Toyota",
    Model:  "Camry",
}

car.Start()                    // Can call Engine's method directly
fmt.Println(car.Horsepower)    // Can access Engine's fields directly

Multiple composition:

type GPS struct {
    Location string
}

func (g GPS) Navigate() {
    fmt.Println("Navigating to", g.Location)
}

type Radio struct {
    Station string
}

func (r Radio) Play() {
    fmt.Println("Playing", r.Station)
}

type ModernCar struct {
    Engine
    GPS
    Radio
    Brand string
}

car := ModernCar{
    Engine: Engine{Horsepower: 250, Type: "V8"},
    GPS:    GPS{Location: "Home"},
    Radio:  Radio{Station: "FM 101"},
    Brand:  "Tesla",
}

car.Start()      // From Engine
car.Navigate()   // From GPS
car.Play()       // From Radio

Interfaces for polymorphism:

// Define behavior without implementation
type Speaker interface {
    Speak() string
}

// Different types implement the interface
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

// Use interface for polymorphic behavior
func MakeSound(s Speaker) {
    fmt.Println(s.Speak())
}

dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}

MakeSound(dog)  // Woof!
MakeSound(cat)  // Meow!

No explicit "implements":

// A type satisfies an interface automatically
// if it has the required methods (implicit implementation)

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

type FileLogger struct {
    filename string
}

// FileLogger implements Writer automatically
// just by having the Write method with matching signature
func (f FileLogger) Write(data []byte) (int, error) {
    // Implementation...
    return len(data), nil
}

// No need to declare "implements Writer"
var w Writer = FileLogger{filename: "log.txt"}

To adhere to an interface: Simply write receiver functions on your struct that match the interface method signatures. That's it - no keywords, no explicit declaration.

// Define interface
type Speaker interface {
    Speak() string
    Listen() string
}

// Any type with these methods automatically satisfies Speaker
type Person struct {
    name string
}

func (p Person) Speak() string {
    return "Hello from " + p.name
}

func (p Person) Listen() string {
    return p.name + " is listening"
}

// Person now implements Speaker - automatically!
var s Speaker = Person{name: "Marko"}

The compiler checks at compile time:

type Greeter interface {
    Greet() string
}

type Robot struct {
    id int
}

func (r Robot) Greet() string {
    return "Beep boop"
}

// This works - Robot has Greet() method
var g Greeter = Robot{id: 1}

// This would fail at compile time if Robot didn't have Greet()
// "cannot use Robot literal (type Robot) as type Greeter in assignment:
//  Robot does not implement Greeter (missing Greet method)"

Multiple interfaces, same type:

type Reader interface {
    Read() string
}

type Writer interface {
    Write(string)
}

type File struct {
    content string
}

func (f *File) Read() string {
    return f.content
}

func (f *File) Write(data string) {
    f.content = data
}

// File implements BOTH Reader and Writer automatically
var r Reader = &File{}
var w Writer = &File{}

Factory functions instead of constructors:

type User struct {
    id    int
    name  string
    email string
}

// Convention: New* functions act as constructors
func NewUser(name, email string) *User {
    return &User{
        id:    generateID(),
        name:  name,
        email: email,
    }
}

// Usage
user := NewUser("Marko", "marko@example.com")

Benefits of composition:

  • Flexibility: Compose types from smaller, reusable components
  • No fragile base class problem: Changes to embedded types don't break hierarchy
  • Clear dependencies: You see exactly what's included in a type
  • Multiple "inheritance": Embed multiple types without diamond problem
  • Simpler mental model: "has-a" relationships are clearer than "is-a"

Important notes:

  • Embedding is not inheritance - it's composition
  • Embedded fields can be accessed directly, but they're still separate
  • Name conflicts in embedded types must be resolved explicitly
  • Interfaces are satisfied implicitly (no explicit declaration needed)

Note: Unlike class-based OOP languages (Java, C++, PHP), Go favors composition and interfaces over inheritance hierarchies.


Maps Are Unordered - Don't Sort Them

Important principle: No one should be sorting a map in Go.

Maps in Go are intentionally unordered data structures. The iteration order is randomized by design.

Key characteristics:

  • No guaranteed order: Maps do not maintain insertion order or any other order
  • Randomized iteration: Each iteration over a map may produce a different order
  • Intentional design: This prevents developers from relying on map order

Why maps are unordered:

m := map[string]int{
    "apple":  1,
    "banana": 2,
    "cherry": 3,
}

// Order is unpredictable and may change between runs
for key, value := range m {
    fmt.Println(key, value)
}
// Output could be: banana, apple, cherry
// Or: cherry, banana, apple
// Or any other order

Go maps vs PHP associative arrays:

Go maps are pure hash maps with NO order guaranteed:

// Pure hash map - NO order guaranteed
m := map[string]int{"b": 2, "a": 1, "c": 3}
// Iteration order: RANDOM and changes between runs

PHP arrays are ordered hash tables - insertion order ALWAYS preserved:

// Ordered hash table - insertion order ALWAYS preserved
$arr = ["b" => 2, "a" => 1, "c" => 3];
foreach ($arr as $k => $v) {
    // Always iterates: b, a, c (insertion order)
}

This is a fundamental difference - PHP arrays maintain insertion order automatically, while Go maps intentionally do not.

If you need order, use the right data structure:

Option 1: Slice of keys (most common)

m := map[string]int{
    "banana": 2,
    "apple":  1,
    "cherry": 3,
}

// Extract and sort keys
keys := make([]string, 0, len(m))
for key := range m {
    keys = append(keys, key)
}
sort.Strings(keys)

// Iterate in sorted order
for _, key := range keys {
    fmt.Println(key, m[key])
}

Option 2: Slice of structs

type Item struct {
    Key   string
    Value int
}

items := []Item{
    {"banana", 2},
    {"apple", 1},
    {"cherry", 3},
}

// Sort the slice
sort.Slice(items, func(i, j int) bool {
    return items[i].Key < items[j].Key
})

for _, item := range items {
    fmt.Println(item.Key, item.Value)
}

Option 3: Separate ordered keys list

type OrderedMap struct {
    keys   []string
    values map[string]int
}

func (om *OrderedMap) Set(key string, value int) {
    if _, exists := om.values[key]; !exists {
        om.keys = append(om.keys, key)
    }
    om.values[key] = value
}

func (om *OrderedMap) Iterate(fn func(string, int)) {
    for _, key := range om.keys {
        fn(key, om.values[key])
    }
}

Why "sorting maps" is wrong:

  • Maps are hash tables optimized for fast lookups, not ordering
  • Trying to "sort a map" shows a misunderstanding of the data structure
  • The correct approach is to extract data and sort it separately
  • If you need order, you probably don't need a map

When to use maps vs slices:

// Use map when:
// - Fast lookups by key are needed
// - Order doesn't matter
// - Checking for existence
usersByID := map[int]User{}

// Use slice when:
// - Order matters
// - Need to iterate in sequence
// - Need sorting
sortedUsers := []User{}

// Use both when:
// - Need fast lookup AND order
userMap := map[int]User{}
orderedIDs := []int{}  // Maintain order separately

Important notes:

  • Map iteration order is deliberately randomized to prevent bugs from order assumptions
  • Never rely on map order in production code
  • If you're tempted to sort a map, reconsider your data structure choice
  • Use slices when order matters, maps when fast lookups matter

Note: Unlike some languages with ordered dictionaries (Python 3.7+), Go maps are intentionally unordered by design.


Receiver Functions (Methods)

Go allows you to define methods on types using receiver functions. A receiver is a special parameter that associates a function with a type.

Basic syntax:

// Type definition
type Rectangle struct {
    Width  float64
    Height float64
}

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

// Usage
rect := Rectangle{Width: 10, Height: 5}
area := rect.Area()  // 50

Value receivers vs Pointer receivers:

Value receiver - receives a copy of the type:

func (r Rectangle) ScaleValue(factor float64) {
    r.Width *= factor   // Modifies the COPY, not the original
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
rect.ScaleValue(2)
fmt.Println(rect.Width)  // Still 10 - original unchanged

Pointer receiver - receives a pointer to the type:

func (r *Rectangle) ScalePointer(factor float64) {
    r.Width *= factor   // Modifies the original
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
rect.ScalePointer(2)
fmt.Println(rect.Width)  // 20 - original modified

When to use pointer receivers:

  • When you need to modify the receiver
  • When the type is large (avoids copying)
  • When the type has pointer receiver methods already (consistency)
  • For most struct methods (common convention)

When to use value receivers:

  • When the type is small and cheap to copy (int, bool, small structs)
  • When the receiver should not be modified
  • For immutable operations
  • For basic types

Methods on any type:

// Can define methods on custom types, not just structs
type Celsius float64

func (c Celsius) ToFahrenheit() float64 {
    return float64(c)*9/5 + 32
}

temp := Celsius(25.0)
fmt.Println(temp.ToFahrenheit())  // 77

Methods on built-in types (via type alias):

type IntSlice []int

func (is IntSlice) Sum() int {
    total := 0
    for _, v := range is {
        total += v
    }
    return total
}

numbers := IntSlice{1, 2, 3, 4, 5}
fmt.Println(numbers.Sum())  // 15

Method chaining:

type Builder struct {
    value string
}

func (b *Builder) Append(s string) *Builder {
    b.value += s
    return b
}

func (b *Builder) String() string {
    return b.value
}

result := new(Builder).
    Append("Hello").
    Append(" ").
    Append("World").
    String()
    
fmt.Println(result)  // Hello World

Multiple methods on same type:

type Account struct {
    balance float64
}

func (a *Account) Deposit(amount float64) {
    a.balance += amount
}

func (a *Account) Withdraw(amount float64) error {
    if amount > a.balance {
        return errors.New("insufficient funds")
    }
    a.balance -= amount
    return nil
}

func (a Account) Balance() float64 {
    return a.balance
}

acc := &Account{}
acc.Deposit(100)
acc.Withdraw(30)
fmt.Println(acc.Balance())  // 70

Receiver naming conventions:

  • Use short names (usually 1-2 letters)
  • Be consistent across methods of same type
  • Common: first letter(s) of type name: r for Rectangle, u for User
  • Use this or self is NOT idiomatic in Go

Important notes:

  • You cannot define methods on types from other packages
  • Pointer and value receivers can be called on both pointers and values (Go handles conversion)
  • Methods are just functions with a receiver parameter
  • Methods enable interface satisfaction for polymorphism

Example showing automatic conversion:

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

// Both work - Go automatically converts
c1 := Counter{}
c1.Increment()  // Go converts to (&c1).Increment()

c2 := &Counter{}
c2.Increment()  // Direct call

Methods vs Functions:

// Function - standalone
func CalculateArea(r Rectangle) float64 {
    return r.Width * r.Height
}

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

// Usage difference
area1 := CalculateArea(rect)  // Function call
area2 := rect.Area()           // Method call (more OOP-like)

Named return values and naked returns:

Go allows you to name return values in the function signature. When you do this, you can use an empty return statement (called a "naked return").

// Named return values
func splitFullName(fullName string) (firstName string, lastName string) {
    parts := strings.Split(fullName, " ")
    firstName = parts[0]
    lastName = parts[1]
    
    return  // Empty return - returns firstName and lastName
}

// Equivalent without named returns
func splitFullName(fullName string) (string, string) {
    parts := strings.Split(fullName, " ")
    firstName := parts[0]
    lastName := parts[1]
    
    return firstName, lastName  // Must explicitly return values
}

How named returns work:

func divide(a, b float64) (result float64, err error) {
    // Named return values are automatically initialized to zero values
    // result = 0.0, err = nil
    
    if b == 0 {
        err = errors.New("division by zero")
        return  // Returns: 0.0, error
    }
    
    result = a / b
    return  // Returns: result, nil
}

Benefits of named returns:

  • Act as documentation (describe what's being returned)
  • Automatically initialized to zero values
  • Can be modified in deferred functions
  • Enable naked returns (shorter code)

When to use naked returns:

// Good - short function, clear intent
func getCoordinates() (x, y int) {
    x = 10
    y = 20
    return
}

// Avoid - long function, harder to track what's returned
func complexCalculation() (result int, err error) {
    // 50 lines of code...
    if someCondition {
        result = 100
        return  // What values are we returning? Have to look at signature
    }
    // More code...
    result = 200
    return  // Easy to lose track
}

Named returns with defer:

func readFile(path string) (content string, err error) {
    file, err := os.Open(path)
    if err != nil {
        return  // Returns "", error
    }
    
    defer func() {
        if closeErr := file.Close(); closeErr != nil && err == nil {
            err = closeErr  // Modify return value in defer!
        }
    }()
    
    data, err := io.ReadAll(file)
    if err != nil {
        return  // Returns "", error
    }
    
    content = string(data)
    return  // Returns content, nil
}

Important notes:

  • Naked returns are controversial in the Go community
  • Use them only in short functions (< 10 lines)
  • They can reduce readability in longer functions
  • Named returns are useful for documentation even without naked returns
  • Defer can modify named return values

Channels - Buffered and Unbuffered

Channels are Go's way of communicating between goroutines. They come in two types: unbuffered and buffered.

Unbuffered Channels:

Unbuffered channels have no capacity - they require both sender and receiver to be ready at the same time (synchronous).

// Create unbuffered channel
ch := make(chan int)

// Sending blocks until someone receives
go func() {
    ch <- 42  // Blocks until main goroutine receives
}()

value := <-ch  // Blocks until goroutine sends
fmt.Println(value)  // 42

Key characteristics of unbuffered channels:

  • Synchronous: Send and receive must happen together
  • Blocking: Sender blocks until receiver is ready, and vice versa
  • Guaranteed delivery: Data is transferred directly between goroutines
  • Zero capacity: make(chan Type) or make(chan Type, 0)

Buffered Channels:

Buffered channels have a capacity - they can hold values without a receiver being immediately ready (asynchronous up to capacity).

// Create buffered channel with capacity 3
ch := make(chan int, 3)

// Can send up to 3 values without blocking
ch <- 1
ch <- 2
ch <- 3
// ch <- 4  // This would block until someone receives

// Receive values
fmt.Println(<-ch)  // 1
fmt.Println(<-ch)  // 2
fmt.Println(<-ch)  // 3

Key characteristics of buffered channels:

  • Asynchronous (up to capacity): Sender doesn't block if buffer has space
  • Blocks when full: Send blocks if buffer is full
  • Blocks when empty: Receive blocks if buffer is empty
  • Specified capacity: make(chan Type, capacity)

Comparison:

// Unbuffered - must have receiver ready
unbuffered := make(chan int)
unbuffered <- 1  // DEADLOCK! No receiver ready

// Buffered - can send without receiver (up to capacity)
buffered := make(chan int, 2)
buffered <- 1  // OK - buffer has space
buffered <- 2  // OK - buffer still has space
// buffered <- 3  // DEADLOCK! Buffer full, no receiver

Practical example - Unbuffered:

func worker(ch chan int) {
    for num := range ch {
        fmt.Println("Processing:", num)
        time.Sleep(time.Second)
    }
}

func main() {
    ch := make(chan int)  // Unbuffered
    
    go worker(ch)
    
    for i := 1; i <= 5; i++ {
        ch <- i  // Blocks until worker receives each value
        fmt.Println("Sent:", i)
    }
    
    close(ch)
}

// Output shows alternating Send/Process (synchronous)

Practical example - Buffered:

func worker(ch chan int) {
    for num := range ch {
        fmt.Println("Processing:", num)
        time.Sleep(time.Second)
    }
}

func main() {
    ch := make(chan int, 3)  // Buffered capacity 3
    
    go worker(ch)
    
    for i := 1; i <= 5; i++ {
        ch <- i  // First 3 don't block
        fmt.Println("Sent:", i)
    }
    
    close(ch)
}

// Output shows first 3 Sent immediately, then waits

When to use unbuffered:

  • When you need guaranteed synchronization between goroutines
  • For handshake/acknowledgment patterns
  • When timing of send/receive matters
  • Default choice (simpler reasoning)

When to use buffered:

  • To decouple sender and receiver timing
  • As a semaphore (limit concurrent operations)
  • To prevent goroutine blocking temporarily
  • For worker pool queues
  • When you know the capacity needed

Common patterns:

Semaphore pattern (limit concurrency):

// Limit to 3 concurrent operations
sem := make(chan struct{}, 3)

for i := 0; i < 10; i++ {
    sem <- struct{}{}  // Acquire
    go func(id int) {
        defer func() { <-sem }()  // Release
        // Do work...
        fmt.Println("Worker", id)
    }(i)
}

Fan-out pattern:

func fanOut(input <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    
    for i := 0; i < workers; i++ {
        ch := make(chan int, 10)  // Buffered for better throughput
        channels[i] = ch
        
        go func(out chan int) {
            for val := range input {
                out <- val * 2
            }
            close(out)
        }(ch)
    }
    
    return channels
}

Important notes:

  • Sending to a closed channel causes panic
  • Receiving from a closed channel returns zero value and false
  • Unbuffered channels guarantee synchronization
  • Buffered channels improve throughput but add complexity
  • Channel buffer size should be based on actual need, not arbitrary
  • Closing channels is sender's responsibility

Checking channel status:

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

// Receive with status check
val, ok := <-ch
fmt.Println(val, ok)  // 1 true

val, ok = <-ch
fmt.Println(val, ok)  // 2 true

val, ok = <-ch
fmt.Println(val, ok)  // 0 false (channel closed and empty)

Note: Channels are Go's primary concurrency primitive for communication between goroutines, following the principle "Don't communicate by sharing memory; share memory by communicating."


Summary

Go combines the best of both worlds:

  • Strong + Static typing: Catches errors early, runs fast, great tooling support
  • Compiled: Fast execution, easy deployment, no runtime dependencies
  • Cross-platform: Write once, deploy everywhere

This makes Go particularly well-suited for:

  • Microservices and backend systems (like your Solar-Log work!)
  • CLI tools and utilities
  • Cloud-native applications
  • High-performance network services
  • DevOps tooling

Last updated: 2026-01-03

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors