Back to Posts
Backend #Golang #Go #Development

Golang Basics for Beginners

Getting started with Go programming language? Here are the fundamental concepts you need to know.

6 min read
Golang Basics for Beginners

Golang Basics for Beginners

Introduction: In a world where software needs to be fast, efficient, and easy to maintain, Go (also known as Golang) has emerged as a powerful solution. Created by Google engineers in 2009, Go was designed to address common frustrations in software development: slow compilation times, complex dependency management, and the difficulty of writing concurrent programs. Whether you're building web servers, command-line tools, or cloud services, Go provides a clean, efficient way to get things done.

Go is a statically typed, compiled programming language that combines the performance of lower-level languages like C with the ease of use found in modern languages like Python. It's known for its simplicity, excellent concurrency support, and fast compilation times, making it a favorite among developers building scalable backend services and cloud infrastructure.


Table of Contents

  1. Why Learn Go?
  2. Setting Up Your Go Environment
  3. Your First Go Program
  4. Basic Types and Variables
  5. Control Structures
  6. Functions
  7. Arrays and Slices
  8. Maps
  9. Structs and Methods
  10. Pointers
  11. Interfaces
  12. Error Handling
  13. Goroutines and Concurrency
  14. Packages and Modules
  15. Best Practices

Why Learn Go?

Before diving into syntax and code, let's understand what makes Go special and why it's worth learning.

Simplicity and Readability

Go was designed with simplicity as a core principle. The language has a small number of keywords (only 25) and a straightforward syntax that's easy to learn and read. There's usually one obvious way to do things, which makes Go code remarkably consistent across different projects and teams.

This simplicity doesn't mean Go is limited. Instead, it means you can focus on solving problems rather than wrestling with language complexity. You can become productive in Go much faster than in many other languages.

Performance

As a compiled language, Go produces fast, efficient binaries. Go programs typically run at speeds comparable to C or C++, making it suitable for performance-critical applications. The Go compiler is also incredibly fast, allowing for rapid development cycles.

Built-in Concurrency

Go's goroutines and channels make concurrent programming straightforward and accessible. While concurrency is notoriously difficult in many languages, Go's design makes it natural and easy to write programs that do multiple things at once efficiently.

Strong Standard Library

Go comes with a comprehensive standard library that handles everything from HTTP servers to JSON parsing, cryptography, and file I/O. You can build complete applications using just the standard library, reducing external dependencies.

Fast Compilation

Go compiles incredibly quickly, even for large codebases. This means you get almost instant feedback when developing, similar to interpreted languages but with the performance of compiled code.

Cross-Platform Support

Go makes it easy to build applications for different operating systems and architectures. With simple build commands, you can create binaries for Windows, macOS, Linux, and more from a single codebase.

Industry Adoption

Go powers critical infrastructure at companies like Google, Uber, Netflix, and Dropbox. Popular projects like Docker, Kubernetes, and Terraform are written in Go. Learning Go opens doors to working on cutting-edge cloud and infrastructure projects.


Setting Up Your Go Environment

Let's get Go installed and configured on your system.

Installing Go

macOS:

# Using Homebrew
brew install go

# Verify installation
go version

Linux:

# Download and extract
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz

# Add to PATH (add to ~/.bashrc or ~/.zshrc)
export PATH=$PATH:/usr/local/go/bin

# Verify installation
go version

Windows:

Download the installer from https://go.dev/dl/ and run it. The installer will set up Go and add it to your PATH automatically.

Setting Up Your Workspace

Go uses a workspace directory to organize your code. By default, this is ~/go.

# View Go environment variables
go env

# Important variables:
# GOPATH - your workspace directory
# GOROOT - where Go is installed
# GOBIN - where compiled binaries go

Your First Project Structure

# Create a project directory
mkdir hello-go
cd hello-go

# Initialize a Go module
go mod init example.com/hello

# This creates a go.mod file that tracks dependencies

Setting Up Your Editor

Popular editors for Go development:

VS Code:

  • Install the "Go" extension by Go Team at Google
  • It provides IntelliSense, debugging, and formatting

GoLand:

  • Full-featured IDE specifically for Go
  • Commercial product by JetBrains

Vim/Neovim:

  • Use vim-go plugin for Go support

Your First Go Program

Let's write and understand a classic "Hello, World!" program in Go.

The Code

Create a file named main.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Understanding Each Part

Package Declaration:

package main

Every Go file starts with a package declaration. The main package is special—it defines an executable program rather than a library. When you build a main package, Go creates an executable binary.

Import Statement:

import "fmt"

The import keyword brings in other packages. Here we import fmt (format), which provides formatted I/O functions. The standard library has many useful packages like net/http, encoding/json, and database/sql.

Multiple imports:

import (
    "fmt"
    "time"
    "math"
)

Main Function:

func main() {
    fmt.Println("Hello, World!")
}

The main function is the entry point of the program. When you run a Go program, execution starts here. Every executable Go program must have exactly one main function in the main package.

Running Your Program

# Run directly (compiles and executes)
go run main.go

# Build an executable
go build main.go

# Run the executable
./main

Adding Comments

package main

import "fmt"

// This is a single-line comment

/*
This is a
multi-line comment
*/

// main is the entry point of the program
func main() {
    // Print a greeting message
    fmt.Println("Hello, World!")
}

Basic Types and Variables

Go is statically typed, meaning every variable has a specific type that must be known at compile time.

Basic Types

Go has several built-in types:

Boolean:

var isActive bool = true
var isComplete bool = false

Numeric Types:

// Integers
var age int = 25              // Platform-dependent size
var count int8 = 127          // -128 to 127
var bigNumber int64 = 1000000 // -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

// Unsigned integers
var positive uint = 42        // Only positive numbers
var byte uint8 = 255          // 0 to 255

// Floating point
var price float32 = 19.99
var precise float64 = 3.14159265359

// Complex numbers
var complex complex64 = 1 + 2i

Strings:

var name string = "John Doe"
var message string = `This is a
multi-line string
using backticks`

Variable Declaration

Go offers multiple ways to declare variables:

Explicit Type Declaration:

var name string = "Alice"
var age int = 30
var isStudent bool = false

Type Inference:

var name = "Alice"    // Go infers string type
var age = 30          // Go infers int type
var price = 19.99     // Go infers float64 type

Short Variable Declaration (inside functions only):

func main() {
    name := "Alice"      // Shorthand, type inferred
    age := 30
    price := 19.99
    
    // Multiple variables
    x, y := 10, 20
    firstName, lastName := "John", "Doe"
}

Declaring Multiple Variables:

var (
    name   string = "Alice"
    age    int    = 30
    salary float64 = 50000.00
)

Constants

Constants are immutable values known at compile time:

const Pi = 3.14159
const AppName = "MyApp"
const MaxRetries = 3

// Multiple constants
const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)

// iota for enumerated constants
const (
    Monday = iota    // 0
    Tuesday          // 1
    Wednesday        // 2
    Thursday         // 3
    Friday           // 4
    Saturday         // 5
    Sunday           // 6
)

Zero Values

In Go, variables declared without an explicit initial value are given their zero value:

var i int       // 0
var f float64   // 0.0
var b bool      // false
var s string    // "" (empty string)
var p *int      // nil

Type Conversion

Go requires explicit type conversion:

var x int = 42
var y float64 = float64(x)  // Convert int to float64
var z uint = uint(x)        // Convert int to uint

// String conversions
import "strconv"

// Int to string
numStr := strconv.Itoa(42)        // "42"

// String to int
num, err := strconv.Atoi("42")    // 42, nil
if err != nil {
    // Handle error
}

// Float to string
floatStr := strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"

Complete Example

package main

import "fmt"

func main() {
    // Different ways to declare variables
    var name string = "Alice"
    var age = 30
    salary := 75000.50
    
    // Constants
    const company = "TechCorp"
    
    // Multiple variables
    firstName, lastName := "John", "Doe"
    
    // Zero values
    var count int
    var active bool
    
    fmt.Println("Name:", name)
    fmt.Println("Age:", age)
    fmt.Println("Salary:", salary)
    fmt.Println("Company:", company)
    fmt.Printf("%s %s\n", firstName, lastName)
    fmt.Println("Count (zero value):", count)
    fmt.Println("Active (zero value):", active)
}

Control Structures

Go has straightforward control structures for decision-making and loops.

If Statements

// Basic if
if age >= 18 {
    fmt.Println("Adult")
}

// If-else
if temperature > 30 {
    fmt.Println("Hot")
} else {
    fmt.Println("Not hot")
}

// If-else if-else
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else if score >= 70 {
    fmt.Println("C")
} else {
    fmt.Println("F")
}

// If with initialization statement
if age := getAge(); age >= 18 {
    fmt.Println("Adult, age:", age)
} else {
    fmt.Println("Minor, age:", age)
}
// age is only available inside the if block

Switch Statements

// Basic switch
day := "Monday"
switch day {
case "Monday":
    fmt.Println("Start of work week")
case "Friday":
    fmt.Println("End of work week")
case "Saturday", "Sunday":
    fmt.Println("Weekend!")
default:
    fmt.Println("Midweek")
}

// Switch with expressions
score := 85
switch {
case score >= 90:
    fmt.Println("A")
case score >= 80:
    fmt.Println("B")
case score >= 70:
    fmt.Println("C")
default:
    fmt.Println("F")
}

// Switch with type
var i interface{} = "hello"
switch v := i.(type) {
case int:
    fmt.Println("Integer:", v)
case string:
    fmt.Println("String:", v)
default:
    fmt.Println("Unknown type")
}

For Loops

Go has only one loop construct: for. But it's flexible enough to handle all loop scenarios.

Traditional for loop:

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

While-style loop:

count := 0
for count < 5 {
    fmt.Println(count)
    count++
}

Infinite loop:

for {
    // Loop forever (until break)
    if condition {
        break
    }
}

Range loop (iterate over collections):

// Array/Slice
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
    fmt.Printf("Index: %d, Value: %d\n", index, value)
}

// Just values
for _, value := range numbers {
    fmt.Println(value)
}

// Just indices
for index := range numbers {
    fmt.Println(index)
}

// String (iterates over runes)
for index, char := range "Hello" {
    fmt.Printf("%d: %c\n", index, char)
}

// Map
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
    fmt.Printf("%s is %d years old\n", name, age)
}

Break and Continue

// Break - exit loop
for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i)
}

// Continue - skip to next iteration
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(i)  // Only prints odd numbers
}

// Labeled breaks (for nested loops)
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer
        }
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

Functions

Functions are the building blocks of Go programs. They help you organize code and make it reusable.

Basic Function Syntax

// Function with parameters and return value
func add(x int, y int) int {
    return x + y
}

// Shortened parameter syntax (same type)
func multiply(x, y int) int {
    return x * y
}

// Function with no parameters
func sayHello() {
    fmt.Println("Hello!")
}

// Function with no return value
func printSum(a, b int) {
    fmt.Println(a + b)
}

Multiple Return Values

Go functions can return multiple values, commonly used for returning results and errors:

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

// Using the function
result, err := divide(10, 2)
if err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Println("Result:", result)
}

// Ignoring return values with _
result, _ := divide(10, 2)  // Ignore error

Named Return Values

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return  // Naked return (returns x and y)
}

// Usage
a, b := split(17)
fmt.Println(a, b)  // 7, 10

Variadic Functions

Functions that accept a variable number of arguments:

func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

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

// Pass a slice
nums := []int{1, 2, 3, 4}
fmt.Println(sum(nums...))  // 10

Anonymous Functions and Closures

// Anonymous function
func() {
    fmt.Println("Anonymous function")
}()

// Assigned to variable
add := func(x, y int) int {
    return x + y
}
fmt.Println(add(3, 5))  // 8

// Closure
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
fmt.Println(c())  // 1
fmt.Println(c())  // 2
fmt.Println(c())  // 3

Defer

The defer statement schedules a function call to be executed after the surrounding function returns:

func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}
// Output:
// Hello
// World

// Multiple defers (LIFO - Last In First Out)
func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// Output:
// 3
// 2
// 1

// Common use: closing resources
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()  // Ensures file is closed
    
    // Read file...
    return nil
}

Arrays and Slices

Arrays and slices are fundamental data structures in Go for storing collections of elements.

Arrays

Arrays have a fixed size determined at compile time:

// Declare array
var numbers [5]int  // Array of 5 integers, initialized to [0 0 0 0 0]

// Initialize with values
primes := [5]int{2, 3, 5, 7, 11}

// Let Go count the elements
primes := [...]int{2, 3, 5, 7, 11}  // Size is 5

// Access elements
fmt.Println(primes[0])  // 2
primes[1] = 13
fmt.Println(primes)     // [2 13 5 7 11]

// Array length
fmt.Println(len(primes))  // 5

// Iterate over array
for i, v := range primes {
    fmt.Printf("Index %d: %d\n", i, v)
}

Slices

Slices are more flexible than arrays—they can grow and shrink:

// Create a slice
var numbers []int  // Empty slice

// Slice literal
primes := []int{2, 3, 5, 7, 11}

// Create slice with make
scores := make([]int, 5)      // Length 5, capacity 5, initialized to [0 0 0 0 0]
scores := make([]int, 3, 10)  // Length 3, capacity 10

// Length and capacity
fmt.Println(len(scores))  // 3
fmt.Println(cap(scores))  // 10

Slice Operations

// Append elements
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
numbers = append(numbers, 5, 6, 7)
fmt.Println(numbers)  // [1 2 3 4 5 6 7]

// Append another slice
more := []int{8, 9, 10}
numbers = append(numbers, more...)
fmt.Println(numbers)  // [1 2 3 4 5 6 7 8 9 10]

// Slicing
nums := []int{0, 1, 2, 3, 4, 5}
slice1 := nums[1:4]   // [1 2 3]
slice2 := nums[:3]    // [0 1 2]
slice3 := nums[3:]    // [3 4 5]
slice4 := nums[:]     // [0 1 2 3 4 5] (full slice)

// Copy slice
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst)  // [1 2 3]

Slice Internals

Slices are references to underlying arrays. Modifying a slice can affect other slices:

original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3]  // [2 3]
slice2 := original[2:4]  // [3 4]

slice1[1] = 99
fmt.Println(original)  // [1 2 99 4 5]
fmt.Println(slice1)    // [2 99]
fmt.Println(slice2)    // [99 4]

Multi-dimensional Slices

// 2D slice (matrix)
matrix := [][]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

fmt.Println(matrix[1][2])  // 6

// Dynamic 2D slice
rows := 3
cols := 4
matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
}

Maps

Maps are Go's built-in hash table or dictionary type. They store key-value pairs.

Creating Maps

// Declare a map
var ages map[string]int  // nil map, can't add elements

// Initialize with make
ages := make(map[string]int)

// Map literal
ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 35,
}

// Alternative syntax
ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

Map Operations

// Add/Update elements
ages["David"] = 28
ages["Alice"] = 31  // Update existing

// Get value
age := ages["Alice"]
fmt.Println(age)  // 31

// Check if key exists
age, exists := ages["Eve"]
if exists {
    fmt.Println("Eve's age:", age)
} else {
    fmt.Println("Eve not found")
}

// Delete element
delete(ages, "Bob")

// Get length
fmt.Println(len(ages))

Iterating Over Maps

ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 35,
}

// Iterate over map (order is random)
for name, age := range ages {
    fmt.Printf("%s is %d years old\n", name, age)
}

// Just keys
for name := range ages {
    fmt.Println(name)
}

// Just values
for _, age := range ages {
    fmt.Println(age)
}

Map as Sets

Since maps return zero values for missing keys, you can use them as sets:

// Set of strings
visited := make(map[string]bool)

// Add to set
visited["page1"] = true
visited["page2"] = true

// Check membership
if visited["page1"] {
    fmt.Println("Already visited page1")
}

// Alternative: using empty struct (more memory efficient)
visited := make(map[string]struct{})
visited["page1"] = struct{}{}

if _, exists := visited["page1"]; exists {
    fmt.Println("Already visited")
}

Structs and Methods

Structs allow you to create custom types by grouping related data together.

Defining Structs

// Basic struct
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

// Struct with embedded types
type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Employee struct {
    Person      // Embedded struct
    ID          int
    Department  string
    Address     Address
}

Creating and Using Structs

// Create struct with field names
person1 := Person{
    FirstName: "John",
    LastName:  "Doe",
    Age:       30,
}

// Create struct with positional values (not recommended)
person2 := Person{"Jane", "Smith", 25}

// Zero value struct
var person3 Person  // All fields are zero values

// Access fields
fmt.Println(person1.FirstName)  // John
person1.Age = 31
fmt.Println(person1.Age)        // 31

// Pointer to struct
p := &Person{"Alice", "Johnson", 28}
fmt.Println(p.FirstName)  // Alice (Go auto-dereferences)

Methods

Methods are functions with a receiver argument:

type Person struct {
    FirstName string
    LastName  string
}

// Method with value receiver
func (p Person) FullName() string {
    return p.FirstName + " " + p.LastName
}

// Method with pointer receiver (can modify the struct)
func (p *Person) UpdateName(first, last string) {
    p.FirstName = first
    p.LastName = last
}

// Usage
person := Person{"John", "Doe"}
fmt.Println(person.FullName())  // John Doe

person.UpdateName("Jane", "Smith")
fmt.Println(person.FullName())  // Jane Smith

When to Use Pointer Receivers

Use pointer receivers when:

  • The method modifies the receiver
  • The struct is large (copying is expensive)
  • You need consistency (if some methods have pointer receivers, all should)
type Circle struct {
    Radius float64
}

// Value receiver - doesn't modify original
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

// Pointer receiver - modifies original
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

// Usage
circle := Circle{Radius: 5}
fmt.Println(circle.Area())  // 78.53975

circle.Scale(2)
fmt.Println(circle.Radius)  // 10

Struct Tags

Struct tags provide metadata for struct fields, commonly used for JSON encoding/decoding:

type User struct {
    ID        int    `json:"id"`
    Username  string `json:"username"`
    Email     string `json:"email,omitempty"`
    Password  string `json:"-"`  // Never include in JSON
}

// JSON encoding
user := User{
    ID:       1,
    Username: "johndoe",
    Email:    "john@example.com",
    Password: "secret",
}

jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData))
// {"id":1,"username":"johndoe","email":"john@example.com"}

Pointers

Pointers store the memory address of a value. They're essential for efficient data manipulation and understanding how Go manages memory.

Pointer Basics

// Declare a variable
x := 42

// Create a pointer to x
var p *int = &x  // & gets the address
fmt.Println(p)   // 0xc000... (memory address)
fmt.Println(*p)  // 42 (* dereferences, gets the value)

// Modify through pointer
*p = 21
fmt.Println(x)   // 21 (x is changed)

// Zero value of pointer is nil
var ptr *int
fmt.Println(ptr)  // <nil>

Pointers with Functions

// Pass by value (doesn't modify original)
func increment(x int) {
    x++
}

// Pass by pointer (modifies original)
func incrementByPointer(x *int) {
    *x++
}

// Usage
num := 5
increment(num)
fmt.Println(num)  // 5 (unchanged)

incrementByPointer(&num)
fmt.Println(num)  // 6 (changed)

Why Use Pointers?

Efficiency: Avoid copying large structures:

type LargeStruct struct {
    data [1000000]int
}

// Inefficient - copies entire struct
func processBad(s LargeStruct) {
    // Process data
}

// Efficient - passes pointer
func processGood(s *LargeStruct) {
    // Process data
}

Mutability: Modify data in place:

type Counter struct {
    value int
}

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

counter := Counter{}
counter.Increment()
fmt.Println(counter.value)  // 1

New Function

The new function allocates memory and returns a pointer:

// Using new
p := new(int)
*p = 42
fmt.Println(*p)  // 42

// Equivalent to:
var i int
p := &i
*p = 42

Interfaces

Interfaces define behavior—a set of method signatures. They're one of Go's most powerful features, enabling polymorphism and flexible code design.

Defining Interfaces

// Interface definition
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implementing the interface (implicit)
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

Using Interfaces

func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}

// Usage
rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}

printShapeInfo(rect)    // Works!
printShapeInfo(circle)  // Works!

Empty Interface

The empty interface interface{} can hold values of any type:

func describe(i interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", i, i)
}

describe(42)
describe("hello")
describe(3.14)
describe([]int{1, 2, 3})

Type Assertions

Extract the underlying concrete type from an interface:

var i interface{} = "hello"

// Type assertion
s := i.(string)
fmt.Println(s)  // hello

// Type assertion with check
s, ok := i.(string)
if ok {
    fmt.Println("String:", s)
}

f, ok := i.(float64)
if !ok {
    fmt.Println("Not a float64")
}

Type Switches

func classify(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %s\n", v)
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

classify(42)
classify("hello")
classify(true)

Common Interfaces

Stringer: Custom string representation:

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

person := Person{"Alice", 30}
fmt.Println(person)  // Alice (30 years)

Error: Error handling:

type error interface {
    Error() string
}

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

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

Error Handling

Go uses explicit error handling rather than exceptions. Errors are values that can be checked and handled.

Basic Error Handling

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

// Usage
result, err := divide(10, 0)
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println("Result:", result)

Creating Custom Errors

import "fmt"

// Using fmt.Errorf
func validateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("age cannot be negative: %d", age)
    }
    if age > 150 {
        return fmt.Errorf("age seems invalid: %d", age)
    }
    return nil
}

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

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

func validateUser(name string, age int) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Value:   name,
            Message: "cannot be empty",
        }
    }
    if age < 18 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "must be 18 or older",
        }
    }
    return nil
}

Error Wrapping (Go 1.13+)

import "fmt"

func readConfig(filename string) error {
    err := openFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read config: %w", err)
    }
    return nil
}

// Check wrapped errors
err := readConfig("config.json")
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("Config file doesn't exist")
}

// Extract specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path error:", pathErr.Path)
}

Panic and Recover

For truly exceptional situations:

// Panic stops normal execution
func mustReadFile(filename string) string {
    data, err := os.ReadFile(filename)
    if err != nil {
        panic(err)  // Panic if critical error
    }
    return string(data)
}

// Recover from panic
func safeExecute() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    // Code that might panic
    mustReadFile("nonexistent.txt")
    fmt.Println("This won't execute if panic occurs")
}

Goroutines and Concurrency

Goroutines are lightweight threads managed by the Go runtime. They make concurrent programming simple and efficient.

Creating Goroutines

import "time"

func sayHello() {
    fmt.Println("Hello")
}

func main() {
    // Run function in a goroutine
    go sayHello()
    
    // Run anonymous function
    go func() {
        fmt.Println("World")
    }()
    
    // Wait for goroutines (naive approach)
    time.Sleep(time.Second)
}

Channels

Channels are the pipes that connect concurrent goroutines:

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

// Send value to channel (blocks until received)
go func() {
    ch <- 42
}()

// Receive value from channel (blocks until sent)
value := <-ch
fmt.Println(value)  // 42

Buffered Channels

// Create buffered channel (holds 3 values)
ch := make(chan int, 3)

// Send without blocking (until buffer is full)
ch <- 1
ch <- 2
ch <- 3

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

Range and Close

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)  // Close channel when done
}

// Receive values until channel is closed
ch := make(chan int, 10)
go fibonacci(10, ch)

for num := range ch {
    fmt.Println(num)
}

Select Statement

Choose from multiple channel operations:

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from channel 1"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from channel 2"
    }()
    
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("timeout")
        }
    }
}

WaitGroup

Properly wait for goroutines to finish:

import "sync"

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // Decrement counter when done
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 5; i++ {
        wg.Add(1)  // Increment counter
        go worker(i, &wg)
    }
    
    wg.Wait()  // Wait for all workers
    fmt.Println("All workers done")
}

Mutex

Protect shared data from concurrent access:

import "sync"

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    counter := SafeCounter{}
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    
    wg.Wait()
    fmt.Println(counter.Value())  // 1000
}

Packages and Modules

Go uses packages to organize code and modules to manage dependencies.

Creating Packages

// File: mathutil/mathutil.go
package mathutil

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

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

Using Packages

// File: main.go
package main

import (
    "fmt"
    "mathutil"  // Your package
)

func main() {
    result := mathutil.Add(3, 5)
    fmt.Println(result)  // 8
    
    // mathutil.subtract(5, 3)  // Error: unexported
}

Go Modules

Initialize a module:

go mod init example.com/myproject

Add dependencies:

# Install and add to go.mod
go get github.com/gin-gonic/gin

The go.mod file tracks dependencies:

module example.com/myproject

go 1.21

require github.com/gin-gonic/gin v1.9.1

Importing Packages

import (
    "fmt"                          // Standard library
    "example.com/myproject/util"   // Local package
    "github.com/gin-gonic/gin"     // External package
)

Best Practices

Code Organization

Use meaningful package names: Short, clear, lowercase names

One concept per package: Each package should have a single, well-defined purpose

Keep packages small: Better to have many small packages than few large ones

Naming Conventions

Variables and functions: camelCase for unexported, PascalCase for exported

// Good
var userCount int
func getUserByID(id int) User

// Exported
var MaxConnections int
func CreateUser(name string) User

Interfaces: Often end in "-er" (Reader, Writer, Closer)

Avoid stuttering: Don't repeat package name in names

// Bad
package user
type UserService struct {}

// Good
package user
type Service struct {}

Error Handling

Check errors: Never ignore errors

// Bad
result, _ := doSomething()

// Good
result, err := doSomething()
if err != nil {
    return err
}

Return errors, don't panic: Panics are for truly exceptional situations

Provide context: Wrap errors with helpful messages

if err != nil {
    return fmt.Errorf("failed to create user: %w", err)
}

Documentation

Document exported items:

// Add returns the sum of two integers.
// It performs basic integer addition.
func Add(a, b int) int {
    return a + b
}

Package documentation:

// Package mathutil provides basic mathematical operations.
package mathutil

Testing

Write tests for your code:

// File: mathutil_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

Run tests:

go test
go test -v  # Verbose output
go test -cover  # Show coverage

Conclusion

You've now learned the fundamentals of Go programming. From basic types and control structures to advanced concepts like goroutines and interfaces, you have the foundation needed to build real-world applications.

Go's simplicity doesn't mean it's limited—major companies rely on Go for critical infrastructure. The language's focus on clarity, performance, and built-in concurrency support makes it an excellent choice for modern software development.

Next Steps

  1. Build projects: Apply what you've learned by building real applications
  2. Read Go code: Study well-written Go projects on GitHub
  3. Learn the standard library: Go's standard library is extensive and well-designed
  4. Explore advanced topics: Reflection, code generation, performance optimization
  5. Contribute to open source: Join the Go community and contribute to projects

Additional Resources

Remember, mastering any programming language takes practice. Write code every day, experiment with different approaches, and don't be afraid to make mistakes. The Go community is welcoming and helpful, so don't hesitate to ask questions and share your learning journey.

Happy coding in Go!

Back to All Posts