Skip to main content

Go lang

  • Open-source programming language developed by Google
  • Features
    • Focus on simplicity, clarity & scalability
    • High performance + Concurrency
    • Batteries included (core features built-in)
    • Static typing (type-sade)
  • Use case
    • Networking & APIs
    • Microservices
    • CLI Tools

Setting

Installation

go version # Check version
go env # Check installation

# Update Go version - Windows (using winget)
winget install -e --id GoLang.Go

# Update Go version - Linux (using snap)
sudo snap install go --classic
sudo snap refresh go

Go commands

# Run the program without creating executable
go run main.go

# Run with module support
go mod init example.com/hello # create go.mod
go build # this creates an executable file
./hello # run the executable
# or simply
go run .
# Update code dependencies
go get -u # update all dependencies
go mod tidy # to clean up unused dependencies
go mod vendor # to create vendor folder
go mod graph # to view dependency graph
go list -m all # to list all dependencies

# To format code
go fmt ./...
# To get help
go help
# To build for different OS/Arch
# Resource: https://golang.org/doc/install/source#environment
GOOS=linux GOARCH=amd64 go build -o hello-linux
GOOS=windows GOARCH=amd64 go build -o hello-windows.exe
GOOS=darwin GOARCH=arm64 go build -o hello-mac

Variables & Data Types

Declare

var x int = 10
y := 20 // short hand - type inferred automatically

// Multiple
var a, b, c string = "foo", "bar", "baz"

// Constants (immutable)
const Pi = 3.14
const (
StatusOK = 200
StatusNotOK = 400
)

Data Types

// Basic types
var age int
var price float64
var complexNum complex128 = 1 + 2i
var isActive bool = true
var name string

// Parse to specific type
var price int = 100
var fine float32 = float32(price) * 0.05

// Composite types
var numbers []int = []int{1, 2, 3}

// Other types
var pointer *int = &age
var person map[string]string = map[string]string{ "name": "Alice", "city": "NY" }
var ch chan int = make(chan int)
var anyType interface{} = "Could be any type"
CategoryTypeZero Value (null)Range
Booleanboolfalsetrue or false
Numeric Types
Integerint032-bit systems: -2^31 to 2^31-1
64-bit systems: -2^63 to 2^63-1
int80-128 to 127
(-2^7 to 2^7-1)
int160-32,768 to 32,767
(-2^15 to 2^15-1)
int320-2,147,483,648 to 2,147,483,647
(-2^31 to 2^31-1)
int640-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
(-2^63 to 2^63-1)
Unsigned Integeruint032-bit systems: 0 to 2^32-1
64-bit systems: 0 to 2^64-1
uint800 to 255
(0 to 2^8-1)
uint1600 to 65,535
(0 to 2^16-1)
uint3200 to 4,294,967,295
(0 to 2^32-1)
uint6400 to 18,446,744,073,709,551,615
(0 to 2^64-1)
uintptr032-bit systems: 0 to 2^32-1
64-bit systems: 0 to 2^64-1
Floatfloat320.0±1.18E-38 to ±3.4E38
float640.0±2.23E-308 to ±1.80E308
Complexcomplex640+0iReal and imaginary parts are float32
complex1280+0iReal and imaginary parts are float64
Stringstring""0 to 2^31-1 bytes
(limited by max slice size)
Pointer*TnilMemory address space
Reference Types
Slice[]Tnil0 to 2^31-1 elements
(implementation dependent)
Mapmap[K]VnilLimited by available memory
Channelchan TnilBuffer size limited by available memory
Interfaceinterface{}nilN/A
Functionfunc()nilN/A
Composite Types
Structstruct{}Zero value of each fieldDepends on field types
Array[n]TArray of zero valuesFixed size defined at declaration
Additional notes
  1. Memory Usage:

    • string: Each string takes 16 bytes overhead plus actual string data
    • slice: 24 bytes overhead plus underlying array
    • map: Implementation dependent, but has overhead per entry
  2. Platform Dependencies:

    • int, uint, and uintptr sizes depend on the platform (32 or 64 bit)
    • Memory addresses and maximum slice sizes are platform dependent
  3. Special Considerations:

    • Floats: Follow IEEE-754 standard
    • Strings: Immutable UTF-8 encoded
    • Maps: Must be initialized with make() before use
    • Channels: Must be initialized with make() before use
  4. SQL Nullable Types:

    sql.NullString    // For nullable string
    sql.NullInt64 // For nullable int64
    sql.NullFloat64 // For nullable float64
    sql.NullBool // For nullable bool
    sql.NullTime // For nullable time.Time

Array & Slice

// Arrays are fixed-length, while Slices are dynamic
var arr = [3]int{1, 2, 3}
var slice = []int{1, 2, 3, 4, 5}

// Updating
arr[0] = 10

// Slicing - if you change the original slice, the sub-slice also changes
subSlice := slice[1:4] // Elements from index 1 to 3
firstThree := slice[:3] // First 3 elements
fromIndex := slice[2:] // From index 2 to end
negativeIndex := slice[len(slice)-3:] // Last 3 elements

// Methods
append(slice, 6, 7) // Append elements
append(slice, anotherSlice...) // Append another slice
len(slice) // Length of slice
cap(slice) // Capacity of slice, underlying array size

// Cloning (deep copy), to avoid modifying original slice
newSlice := make([]int, len(slice)) // Create new slice with same length
copy(newSlice, slice) // Deep copy

Map

// map[keyType]valueType

// Creating
m := map[string]int{"apple": 1, "banana": 2}
// Empty map with initial capacity of 5 (optional)
m := make(map[string]int, 5)

// Methods
len(m) // Number of key-value pairs

// Updating
m["orange"] = 3 // Add new key-value
m["apple"] = 10 // Update existing key
delete(m, "banana") // Delete key

// Accessing
fmt.Println(m["apple"]) // Get value by key
value, exists := m["banana"] // Check if key exists

for key, value := range m { // Iterate over map
fmt.Println(key, value)
}

// map with complex types
complexMap := map[string][]int{
"primes": {2, 3, 5, 7},
"evens": {2, 4, 6, 8},
}
complexMap["primes"][0] = 11 // Update first element of "primes" slice

Type Alias

type MyInt = int // MyInt is an alias for int
var age MyInt = 30

func printAge(a MyInt) {
fmt.Println(a)
}

printAge(age) // Works, since MyInt is an alias for int
printAge(MyInt(25)) // Also works, explicit conversion
printAge(25) // Error, int is not MyInt

type bookMap = map[string]string // Alias for map type
var books bookMap = map[string]string{
"978-3-16-148410-0": "The Go Programming Language",
"978-0-13-419044-0": "Introducing Go",
}

Reading & Writing

import "fmt"

fmt.Print("Hello") // Print without newline
fmt.Println("Hello, World!") // Print with newline
fmt.Printf("Age: %d", age) // Formatted print
fmt.Printf("Price: %.2f", price) // Format float with 2 decimal places
fmt.Printf(`Lorem ipsum
dolor sit amet`) // Multi-line string, special characters like \n are preserved

var input string
fmt.Scan(&input) // Read input until space
fmt.Scanln(&input) // Read input until newline

// Formatting & Errors
formattedStr := fmt.Sprintf("Name: %s", name) // Format to string
formattedErr := fmt.Errorf("Error: %s", "something went wrong") // Create error with formatted message
fmt.Print(formattedStr, formattedErr)

Functions

Main function

main function is the entry point of a Go program.

// this function is required in every executable program
package main

import "fmt"

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

Defining Functions

func add(x int, y int) int {
return x + y
}

var result int = add(3, 5) // result is 8
// Multiple return values
func swap(a, b string) (string, string) {
return b, a
}

x, y := swap("first", "second") // x is "second", y is "first"
// Single return value with named return variable
func getUserInput(info string) (value float32) {
fmt.Print(info)
fmt.Scan(&value)
return value
}

var input float32 = getUserInput("Enter a number: ")
fmt.Println("You entered:", input) // e.g. You entered: 42.5
// Using named return values as documentation and to avoid using constraint variable's name
func calculateFinancials(revenue float32, expenses float32, taxRate float32)
(ebt float32, profit float32, ratio float32) {
ebt = revenue - expenses // Earn Before Tax
profit = ebt * (1 - taxRate/100)
ratio = ebt / profit

return ebt, profit, ratio
}

ebt, profit, ratio := calculateFinancials(10000, 4000, 20)
fmt.Println(ebt, profit, ratio) // 6000 4800 1.25

Advanced Function Concepts

// Variadic function (accepts variable number of arguments)
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}

result := sum(1, 2, 3, 4, 5) // result is 15
// Anonymous function assigned to a variable
multiply := func(a int, b int) int {
return a * b
}

result := multiply(4, 5) // result is 20
// Higher-order function (function that takes another function as an argument)
func applyOperation(x int, y int, operation func(int, int) int) int {
return operation(x, y)
}

result := applyOperation(10, 5, func(a int, b int) int {
return a - b
}) // result is 5
// Returning a function from another function
func makeMultiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15

// Closure (function that captures variables from its surrounding scope)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}

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

Control flow

Conditional

if age > 18 {
fmt.Println("Adult")
} else if (age > 14) {
fmt.Println("Minor")
} else {
fmt.Println("Child")
}
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday":
fmt.Println("Midweek")
case "Sunday":
fmt.Println("End of the week")
default:
fmt.Println("Not the day")
}
// Switch with conditions - no need for break
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
default:
fmt.Println("C or below")
}
// fallthrough - continue to next case
// The fallthrough must be the last statement in the case block
switch {
case score >= 90:
fmt.Println("A")
fallthrough
case score >= 80:
fmt.Println("B")
fallthrough
default:
fmt.Println("C or below")
}
// If score is 95, output will be:
// A
// B
// C or below

Loops

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

// Infinity -> for condition {}
for {
// do something and break or return
}
// Iterate over collection
for index, value := range collection {
fmt.Println(index, value)
}
// If index is not needed
for _, value := range collection {
fmt.Println(value)
}
// Nested loops with label, this can be used with break and continue
outer:
for userInput != 0 {
switch userInput {
case 1:
// something
// ...
default:
break outer // exit loop
}
}

File & I/O

import (
"fmt"
"os"
)

func main() {
// Create or open a file
file, err := os.OpenFile("example.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close() // ensure the file is closed when done

// Write to the file
_, err = file.WriteString("Hello, Go File Handling!\n")
if err != nil {
fmt.Println("Error writing to file:", err)
return
}

// Read from the file
data, err := os.ReadFile("example.txt")
if err != nil {
fmt.Println("Error reading file:", err)
return
}
fmt.Println("File content:\n", string(data))
}
// Example with read, write numbers and use panic
func getBalanceFromFile() float32 {
data, err := os.ReadFile("balance.txt")
if err != nil {
if os.IsNotExist(err) {
return 0 // file doesn't exist, return 0 balance
}
// Use this if you want to stop execution on error and see the error
// panic: example.txt: no such file or directory
panic(err) // unexpected error
}
var balance float32
fmt.Sscanf(string(data), "%f", &balance)
// balance, _ := strconv.parseFloat(string(data), 64) // alternative
return balance
}

func updateBalanceToFile(balance float32) {
err := os.WriteFile("balance.txt", []byte(fmt.Sprintf("%f", balance)), 0644)
if err != nil {
panic(err) // unexpected error
}
}

Get huge user input

import (
"bufio"
"fmt"
"os"
"strings"
)

func getUserInput() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter text: ")
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}

Save and load JSON data

import (
"encoding/json"
"fmt"
"os"
"time"
)

// json struct tags for custom field names. This feature is useful for APIs
type User struct {
Name string `json:"name"`
Email string `json:"email"`
CreateAt time.Time `json:"created_at"`
}

func saveUserToFile(user User) {
data, err := json.Marshal(user)
if err != nil {
panic(err)
}
err = os.WriteFile("user.json", data, 0644)
if err != nil {
panic(err)
}
}

func loadUserFromFile() User {
data, err := os.ReadFile("user.json")
if err != nil {
panic(err)
}
var user User
err = json.Unmarshal(data, &user)
if err != nil {
panic(err)
}
return user
}

Struct

// Structs are used to group related data together
type Person struct {
Name string
Age int
}

// Constructor function
func NewPerson(name string, age int) Person {
return Person{Name: name, Age: age}
}

// Method associated with Person struct
func (p Person) Greet() {
fmt.Println("Hello, my name is", p.Name)
}

// Update age method with pointer receiver
func (p *Person) HaveBirthday() {
p.Age++ // (*p).Age++ also works
}

// Create instance
p := Person{Name: "Alice", Age: 30}
p.Greet() // Output: Hello, my name is Alice
p.HaveBirthday()
fmt.Println(p.Age) // Output: 31
// Struct Embedding
type Address struct {
City, State string
}

type Employee struct {
Person // Embedding Person struct
Address // Embedding Address struct
Position string
}

e := Employee{
Person: Person{Name: "Bob", Age: 25},
Address: Address{City: "New York", State: "NY"},
Position: "Developer",
}

fmt.Println(e.Name) // Access embedded field: Bob
fmt.Println(e.City) // Access embedded field: New York
e.Greet() // Call method from embedded struct
// Anonymous Struct
anon := struct {
Title string
Author string
}{
Title: "Go Programming",
Author: "John Doe",
}

fmt.Println(anon.Title) // Output: Go Programming

Packages & Imports in Go

// Single import
import "fmt"

// Multiple imports
import (
"fmt"
"net/http"
)

// Alias import
import h "net/http"

// Dot import (pollutes namespace — avoid in real code)
import . "fmt"

// Blank import: execute package init() for side-effects (e.g., SQL drivers)
import _ "github.com/lib/pq"
Rules to remember
  • Imports must be compile-time constants (string literals).
  • Go auto-runs each imported package’s init() before main().

Modules 101 (Go Modules on by default)

# 1) Start a module at repo root
go mod init github.com/you/project

# 2) Add a dependency (adds + pins in go.mod/go.sum)
go get github.com/gorilla/mux@v1.8.1 # or @latest

# 3) Clean up and resolve versions
go mod tidy
Semantic Import Versioning
  • For v0/v1: module github.com/you/project

  • For v2+: module path MUST include /vN, and you import with that:

    • module github.com/gorilla/mux/v2
    • import "github.com/gorilla/mux/v2"

Standard vs Third-Party

import (
"context" // standard lib
"github.com/google/uuid" // third-party module
)

Style: group stdlib first, then third-party, blank line between. go fmt or your editor will sort/format for you.

Common Import Patterns

Alias to avoid name collisions

import jsoniter "github.com/json-iterator/go"

Side-effect init (drivers, registers)

import _ "github.com/mattn/go-sqlite3"

Platform/file-scoped imports Use build tags or file suffixes instead of conditional imports:

  • foo_linux.go, foo_windows.go
  • //go:build linux at top of file

go.mod Essentials

module github.com/you/app

go 1.22

require (
github.com/gorilla/mux v1.8.1
github.com/google/uuid v1.6.0
)

replace example.com/old => ./local/old # local path
replace example.com/x => example.com/y v0.5.0

Handy ops

go mod tidy           # prune/add deps based on imports
go mod why -m <mod> # why is this here?
go list -m all # list module graph

Private Modules (GitHub/GitLab, etc.)

# Tell Go to bypass proxy + sumdb for your org
go env -w GOPRIVATE=github.com/your-org/*

# (Optional) disable checksums for private domain
go env -w GONOSUMDB=github.com/your-org/*

# Auth: ensure git can auth (SSH keys or PAT in credential helper)

Then import normally:

import "github.com/your-org/secretpkg"

Proxy & Caching

go env GOPROXY        # check current proxy (often https://proxy.golang.org,direct)
go env -w GOPROXY=direct # fetch straight from VCS (useful for private)

Vendor Mode (pin + vendored source)

go mod vendor         # creates ./vendor with deps
go build -mod=vendor # prefer vendor over network

Good for hermetic builds or locked CI environments.

Multi-Module Repos (Monorepos)

At root:

go work init ./svc-a ./svc-b
go work use ./lib-common

Now each module can import others without publishing interim versions.

Typical Errors & Quick Fixes
  • cannot find module providing package ...

    • Run go get <module> or check the import path/version (e.g., missing /v2).
  • Checksum mismatch (verifies go.sum: mismatch)

    • Clear cache if necessary: go clean -modcache; verify you’re not behind a MITM proxy; ensure GOPRIVATE is set for private modules.
  • import cycle not allowed

    • Refactor shared types/funcs into a third package; avoid cross-layer imports.
  • no required module provides package in a subdir

    • Run go mod tidy in the module root; ensure your file belongs to the intended module.

Quick Checklist (when something breaks)

  1. go mod tidy
  2. Check import path (esp. /v2 or higher).
  3. Confirm GOPRIVATE, GOPROXY, auth.
  4. go clean -modcache (if checksums act up).
  5. If monorepo, ensure go.work includes all local modules.

Exports in Go

The Core Rule: Capitalization = Exported

In Go, visibility (exporting) depends entirely on the first letter of the identifier.

VisibilityExampleAccessible From
ExportedName, DoSomethingOther packages
Unexportedname, doSomethingSame package only
greeting/greeting.go
package greeting

// Exported (public)
func SayHello() string {
return "Hello!"
}

// Unexported (private)
func whisper() string {
return "psst..."
}
main.go
package main

import (
"fmt"
"example.com/app/greeting"
)

func main() {
fmt.Println(greeting.SayHello()) // ✅ works
// greeting.whisper() ❌ not accessible
}

Exporting Variables and Constants

package mathx

const Pi = 3.14159 // exported
var Count = 42 // exported
var hiddenValue = 99 // unexported
package main

import "example.com/mathx"

func main() {
fmt.Println(mathx.Pi) // ✅
fmt.Println(mathx.Count) // ✅
// fmt.Println(mathx.hiddenValue) ❌ private
}

Exporting Structs and Their Fields

Struct visibility is two-layered:

  • The struct type must be exported to use it outside the package.
  • Its fields must also be exported if you want to access them externally.
package model

type User struct {
Name string // exported field
age int // unexported field
}

func NewUser(name string, age int) User {
return User{Name: name, age: age}
}
package main

import (
"fmt"
"example.com/app/model"
)

func main() {
u := model.NewUser("Fee", 22)
fmt.Println(u.Name) // ✅
// fmt.Println(u.age) ❌ private
}

Exporting Interfaces

package repo

type Storable interface { // exported
Save() error
}

type private interface { // unexported
load()
}
Good Practice Tips
  • Capitalize intentionally. Don’t export unless it’s part of your package’s public API.
  • Group exports meaningfully. If others will use it, add doc comments (// SayHello ...).
  • Hide internals. Keep helper functions and constants unexported.
  • Use constructors. For structs with unexported fields, expose a constructor (e.g., NewUser).
  • Don’t overexpose. Export only what’s needed for other packages to function cleanly.

Pointers & Reference Types

  • Modify variables in functions without returning them
  • Efficiently pass large data structures (slices, maps, structs) without copying
  • Implement linked data structures (linked lists, trees)

Pointers Basics

var a int = 42

var p *int = &a // p holds the address of a
fmt.Println(*p) // Dereference p to get value: 42

*p = 100 // Change value at address p points to
fmt.Println(a) // a is now 100

var s []int = []int{1, 2, 3}
var ps *[]int = &s // ps holds the address of slice s
(*ps)[0] = 10 // Modify slice through pointer
fmt.Println(s) // Output: [10, 2, 3]

var m map[string]int = map[string]int{"one": 1, "two": 2}
var pm *map[string]int = &m // pm holds the address of map m
(*pm)["one"] = 10 // Modify map through pointer
fmt.Println(m) // Output: map[one:10 two:2]
var f func(int) int = func(x int) int { return x * x }
var pf *func(int) int = &f // pf holds the address of function f
fmt.Println((*pf)(5)) // Call function through pointer: 25
// Pointer to struct
type Point struct {
X, Y int
}

var pt Point = Point{X: 1, Y: 2}
var ppt *Point = &pt // ppt holds the address of struct pt
ppt.X = 10 // Modify struct field through pointer
fmt.Println(pt) // Output: {10 2}

Pointer in Function Arguments

func increment(x *int) {
*x = *x + 1 // Dereference pointer to modify original value
}

var num int = 5
increment(&num) // Pass address of num
fmt.Println(num) // Output: 6

Nil Pointers

  • A pointer that does not point to any memory location
var p *int = nil // nil pointer
if p == nil {
fmt.Println("p is nil")
}

Unsafe Package

  • Allows low-level memory manipulation, but use with caution
import "unsafe"
var i int = 42
var p unsafe.Pointer = unsafe.Pointer(&i)
var b *byte = (*byte)(p) // Convert to pointer of different type

Interface in Go

  • Defines a set of method signatures (behavior)
  • Types implement interfaces implicitly (no implements keyword)
  • Enables polymorphism (different types can be treated uniformly)
  • Used for abstraction, decoupling, and mocking in tests
  • Can be composed (interfaces can embed other interfaces)

Defining and Implementing Interfaces

type Shape interface {
Area() float64
Perimeter() float64
}

// Circle implements Shape because it has Area and Perimeter methods
type Circle struct {
Radius float64
}

func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14 * c.Radius
}
func PrintShapeInfo(s Shape) {
fmt.Printf("Area: %f, Perimeter: %f\n", s.Area(), s.Perimeter())
}
c := Circle{Radius: 5}
PrintShapeInfo(c) // Works because Circle implements Shape

Interface Composition

type Colored interface {
Color() string
}
type ColoredShape interface {
Shape
Colored
}

type Square struct {
Side float64
ColorName string
}
func (s Square) Area() float64 {
return s.Side * s.Side
}
func (s Square) Perimeter() float64 {
return 4 * s.Side
}
func (s Square) Color() string {
return s.ColorName
}
func PrintColoredShapeInfo(cs ColoredShape) {
fmt.Printf("Area: %f, Perimeter: %f, Color: %s\n", cs.Area(), cs.Perimeter(), cs.Color())
}
sq := Square{Side: 4, ColorName: "Red"}
PrintColoredShapeInfo(sq) // Works because Square implements ColoredShape

Type Assertions and Type Switches

func Describe(i interface{}) {
// val, ok := i.(int) // Type assertion, panics if wrong type
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case Shape:
fmt.Printf("Shape with Area: %f\n", v.Area())
default:
fmt.Println("Unknown type")
}
}
Describe(42)          // Integer: 42
Describe("hello") // String: hello
Describe(Circle{Radius: 3}) // Shape with Area: 28.260000

Empty Interface

func PrintAnything(i interface{}) {
fmt.Println(i)
}
PrintAnything(123)          // 123
PrintAnything("hello") // hello
PrintAnything([]int{1, 2, 3}) // [1 2 3]
PrintAnything(map[string]int{"a": 1}) // map[a:1]

Using Interfaces for Mocking in Tests

type Database interface {
GetUser(id int) (User, error)
}
type UserService struct {
DB Database
}
func (us *UserService) GetUserName(id int) (string, error) {
user, err := us.DB.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
type MockDB struct{}
func (mdb MockDB) GetUser(id int) (User, error) {
return User{ID: id, Name: "Mock User"}, nil
}
func TestGetUserName(t *testing.T) {
mockDB := MockDB{}
userService := UserService{DB: mockDB}
name, err := userService.GetUserName(1)
if err != nil || name != "Mock User" {
t.Errorf("Expected 'Mock User', got '%s'", name)
}
}

Generics

  • Since Go 1.18, Go supports generics: type parameters for functions, methods, and types.
  • Basically: "write once, use for any type.""

Generic Functions

func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"hi", "yo"})
Breakdown
  • T → type parameter name (like a variable for a type).
  • [T any] → “for any type T”.
  • Inside the function, you can use T like a normal type.

Type Constraints

  • Type constraints define which types are allowed.
  • The simplest one is any (a.k.a. interface{}).
  • Only types supporting the used operator (+, -, etc.) can go in the constraint.
type Number interface {
~int | ~float64
}

func Add[T Number](a, b T) T {
return a + b
}
fmt.Println(Add(3, 5))         // int
fmt.Println(Add(2.5, 3.1)) // float64
// Add("hi", "bye") ❌ not allowed

Generic Structs

type Pair[T any, U any] struct {
First T
Second U
}

func main() {
p := Pair[int, string]{First: 1, Second: "one"}
fmt.Println(p)
}

Generic Methods

type Box[T any] struct {
value T
}

func (b Box[T]) Get() T {
return b.value
}

func (b *Box[T]) Set(v T) {
b.value = v
}
b := Box[int]{value: 10}
fmt.Println(b.Get()) // 10
b.Set(20)

Generic Interfaces

type Container[T any] interface {
Get() T
Set(v T)
}
// Or use "type approximation" (the `~` operator) for "underlying type" matching:
type IntLike interface {
~int | ~int32 | ~int64
}

Instantiating Generics

  • You can explicitly specify type arguments, or let the compiler infer them:
  • Inference works in most cases unless ambiguity exists.
Add[int](1, 2)   // explicit
Add(1, 2) // inferred

Type Sets & Constraints Syntax

SymbolMeaningExample
anyany type[T any]
|union (OR)~int | ~float64
~underlying type~string
comparablesupports == and !=[T comparable]
// Example using 'comparable'
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}

Generic Type Embedding

// You can mix generics + embedding

type Stack[T any] struct {
data []T
}

func (s *Stack[T]) Push(v T) {
s.data = append(s.data, v)
}

func (s *Stack[T]) Pop() T {
last := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return last
}
s := Stack[string]{}
s.Push("go")
s.Push("rocks")
fmt.Println(s.Pop()) // "rocks"

Common Patterns

// Map-like utility
func Map[T any, U any](in []T, fn func(T) U) []U {
out := make([]U, len(in))
for i, v := range in {
out[i] = fn(v)
}
return out
}
func Reduce[T any, U any](in []T, fn func(U, T) U, init U) U {
acc := init
for _, v := range in {
acc = fn(acc, v)
}
return acc
}

Tips and Pitfalls

  1. No runtime type info — generics are erased at compile time (no reflection magic).
  2. Don’t overuse — Go still favors simplicity. If interfaces or duplication are clearer, use them.
  3. Performance — generics are efficient; they’re compiled into specialized versions per type.
  4. Exporting generic types works the same as normal: start with uppercase.
When to Use Generics
  • Repeated logic across multiple types
  • Collections (slices, stacks, maps)
  • Utilities (Map, Filter, Reduce)
  • Strong typing over interface{}

Concurrency

  • Goroutines: super-light threads managed by the Go runtime.
  • Channels: typed pipes for communicating between goroutines.
  • select: wait on multiple channel ops.
  • Context: cancellation + deadlines across goroutines.
  • Prefer sharing by communicating; fall back to locks when state is truly shared.

1. Goroutines

  • Goroutine exits when the function returns.
  • Don’t let main exit early—use sync.WaitGroup, errgroup, or channel signals.
go fn()          // fire-and-forget
go func(x int) { /* ... */ }(42)

2. Channels

ch := make(chan int)      // unbuffered: send/recv must rendezvous
chB := make(chan int, 8) // buffered: capacity 8

Send / Receive

ch <- 10        // send
v := <-ch // receive
v, ok := <-ch // ok=false if channel closed and drained

Closing

close(ch)       // sender closes to signal "no more values"
for v := range ch { ... } // drains until closed
Rules
  • Only senders should close a channel.
  • Don’t close a channel twice; don’t send on a closed channel.

3. select Basics

Use select for timeouts, cancellation, multi-source fan-in, or avoiding deadlocks.

select {
case v := <-ch1:
// got value
case ch2 <- x:
// sent value
case <-time.After(200 * time.Millisecond):
// timeout
default:
// non-blocking path
}

4. WaitGroup (join goroutines)

var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func(i int) {
defer wg.Done()
// work
}(i)
}
wg.Wait()

5. Mutex / RWMutex (shared state)

type Counter struct {
mu sync.RWMutex
n int
}
func (c *Counter) Inc() { c.mu.Lock(); c.n++; c.mu.Unlock() }
func (c *Counter) Get() int { c.mu.RLock(); defer c.mu.RUnlock(); return c.n }
Rule of thumb
  • Channels: ownership transfer / sequencing.
  • Mutexes: protect shared in-memory state.

6. Context (timeouts & cancellation)

Pass ctx as first param: Func(ctx context.Context, ...).

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case <-ctx.Done():
return ctx.Err()
case v := <-ch:
_ = v
}

7. errgroup (structured concurrency)

  • Cancels siblings on first error.
  • Cleaner than manual WaitGroup + error channels.
g, ctx := errgroup.WithContext(context.Background())

for _, url := range urls {
url := url
g.Go(func() error {
// respect ctx for cancellation
return fetch(ctx, url)
})
}
if err := g.Wait(); err != nil { /* handle */ }

8. Worker Pool (bounded parallelism)

func workerPool[T any, U any](ctx context.Context, in <-chan T, n int, fn func(context.Context, T) (U, error)) (<-chan U, <-chan error) {
out := make(chan U)
errc := make(chan error, 1)

var wg sync.WaitGroup
wg.Add(n)

for i := 0; i < n; i++ {
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case v, ok := <-in:
if !ok { return }
u, err := fn(ctx, v)
if err != nil {
select { case errc <- err: default: } // send first error
return
}
select {
case out <- u:
case <-ctx.Done():
return
}
}
}
}()
}

go func() { wg.Wait(); close(out) }()

return out, errc
}

9. Pipelines, Fan-out/Fan-in

Generator → Workers → Merge

func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums { out <- n }
close(out)
}()
return out
}

func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for v := range in { out <- v*v }
close(out)
}()
return out
}

func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

wg.Add(len(cs))
for _, c := range cs {
go func(c <-chan int) {
defer wg.Done()
for v := range c { out <- v }
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}

// usage
in := gen(1,2,3,4)
c1 := sq(in)
c2 := sq(in) // fan-out across workers → careful: you can’t reuse drained channel; typically split upstream
res := merge(c1, c2)
for v := range res { fmt.Println(v) }

In real fan-out, duplicate the work upstream or distribute work to multiple workers reading from the same in.

10. Timeouts, Heartbeats, Rate Limit

// Timeout per op
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
select {
case v := <-work:
case <-ctx.Done():
}
// Ticker heartbeat
tick := time.NewTicker(1 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C: /* heartbeat */
case <-ctx.Done(): return
}
}
// Semaphore (limit concurrency)
sem := make(chan struct{}, 8) // capacity = max concurrency
sem <- struct{}{} // acquire
// work
<-sem // release

11. sync/atomic (lock-free counters/flags)

  • Keep atomic state self-contained; avoid mixing atomics and mutexes on same data.
var ready atomic.Bool
ready.Store(true)
if ready.Load() { /* ... */ }

var n atomic.Int64
n.Add(1)
v := n.Load()

12. Avoid Leaks & Deadlocks

Leaks (goroutine stuck on send/recv):

  • Always select on <-ctx.Done() for long-lived goroutines.
  • Ensure every send has a receiver path (buffer or consumer).
  • Close channels from the producer side + drain consumers.

Deadlocks

  • Don’t hold a mutex while doing channel send/recv or blocking calls.
  • Keep a strict lock ordering.

13. Memory Model (quick hits)

  • Happens-before via:

    • Channel send → receive
    • Mutex unlock → lock
    • WaitGroup.Wait() after Done()
    • Atomic ops establish order on that variable
  • Without these, concurrent writes/reads are data races.

14. Testing & Debugging

  • Race detector: go test -race ./... (or go run -race .)
  • -cpu: run tests on multiple P’s go test -cpu=1,2,4
  • pprof: identify goroutine leaks / blocking profiles
  • go vet: basic concurrency lint

15. Patterns That Slap (and when to pick them)

PatternWhen to use
Worker PoolMany independent tasks; cap concurrency
PipelineStaged processing with backpressure
Fan-inMerge results from multiple sources
ErrgroupStructured tasks where any error cancels all
SemaphoreThrottle external I/O or CPU-heavy ops
Ticker/TimerHeartbeats, timeouts, periodic jobs

16. Compact Examples

Cancelable producer

func produce(ctx context.Context) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; ; i++ {
select {
case out <- i:
case <-ctx.Done():
return
}
}
}()
return out
}

Context-aware handler

func handle(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case j, ok := <-jobs:
if !ok { return nil }
if err := j.Do(ctx); err != nil { return err }
}
}
}

17. Quick Checklist

  • Do you propagate context.Context everywhere async?
  • Are all long-running goroutines selecting on ctx.Done()?
  • Are channels closed by producers only?
  • Is shared state protected (mutex/atomic) or eliminated (ownership via channel)?
  • Did you run -race?

Tesing

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

Resources


  • Arrays, Slices, Maps
  • Concurrency (goroutines, channels)
  • REST API