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"
Category | Type | Zero Value (null) | Range |
---|---|---|---|
Boolean | bool | false | true or false |
Numeric Types | |||
Integer | int | 0 | 32-bit systems: -2^31 to 2^31-1 64-bit systems: -2^63 to 2^63-1 |
int8 | 0 | -128 to 127 (-2^7 to 2^7-1) | |
int16 | 0 | -32,768 to 32,767 (-2^15 to 2^15-1) | |
int32 | 0 | -2,147,483,648 to 2,147,483,647 (-2^31 to 2^31-1) | |
int64 | 0 | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (-2^63 to 2^63-1) | |
Unsigned Integer | uint | 0 | 32-bit systems: 0 to 2^32-1 64-bit systems: 0 to 2^64-1 |
uint8 | 0 | 0 to 255 (0 to 2^8-1) | |
uint16 | 0 | 0 to 65,535 (0 to 2^16-1) | |
uint32 | 0 | 0 to 4,294,967,295 (0 to 2^32-1) | |
uint64 | 0 | 0 to 18,446,744,073,709,551,615 (0 to 2^64-1) | |
uintptr | 0 | 32-bit systems: 0 to 2^32-1 64-bit systems: 0 to 2^64-1 | |
Float | float32 | 0.0 | ±1.18E-38 to ±3.4E38 |
float64 | 0.0 | ±2.23E-308 to ±1.80E308 | |
Complex | complex64 | 0+0i | Real and imaginary parts are float32 |
complex128 | 0+0i | Real and imaginary parts are float64 | |
String | string | "" | 0 to 2^31-1 bytes (limited by max slice size) |
Pointer | *T | nil | Memory address space |
Reference Types | |||
Slice | []T | nil | 0 to 2^31-1 elements (implementation dependent) |
Map | map[K]V | nil | Limited by available memory |
Channel | chan T | nil | Buffer size limited by available memory |
Interface | interface{} | nil | N/A |
Function | func() | nil | N/A |
Composite Types | |||
Struct | struct{} | Zero value of each field | Depends on field types |
Array | [n]T | Array of zero values | Fixed size defined at declaration |
-
Memory Usage:
string
: Each string takes 16 bytes overhead plus actual string dataslice
: 24 bytes overhead plus underlying arraymap
: Implementation dependent, but has overhead per entry
-
Platform Dependencies:
int
,uint
, anduintptr
sizes depend on the platform (32 or 64 bit)- Memory addresses and maximum slice sizes are platform dependent
-
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
-
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"
- Imports must be compile-time constants (string literals).
- Go auto-runs each imported package’s
init()
beforemain()
.
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
-
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.
-
cannot find module providing package ...
- Run
go get <module>
or check the import path/version (e.g., missing/v2
).
- Run
-
Checksum mismatch (
verifies go.sum: mismatch
)- Clear cache if necessary:
go clean -modcache
; verify you’re not behind a MITM proxy; ensureGOPRIVATE
is set for private modules.
- Clear cache if necessary:
-
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.
- Run
Quick Checklist (when something breaks)
go mod tidy
- Check import path (esp.
/v2
or higher). - Confirm
GOPRIVATE
,GOPROXY
, auth. go clean -modcache
(if checksums act up).- 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.
Visibility | Example | Accessible From |
---|---|---|
Exported | Name , DoSomething | Other packages |
Unexported | name , doSomething | Same package only |
package greeting
// Exported (public)
func SayHello() string {
return "Hello!"
}
// Unexported (private)
func whisper() string {
return "psst..."
}
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()
}
- 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"})
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
Symbol | Meaning | Example |
---|---|---|
any | any type | [T any] |
| | union (OR) | ~int | ~float64 |
~ | underlying type | ~string |
comparable | supports == 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
- No runtime type info — generics are erased at compile time (no reflection magic).
- Don’t overuse — Go still favors simplicity. If interfaces or duplication are clearer, use them.
- Performance — generics are efficient; they’re compiled into specialized versions per type.
- Exporting generic types works the same as normal: start with uppercase.
- 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—usesync.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
- 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 }
- 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()
afterDone()
- Atomic ops establish order on that variable
-
Without these, concurrent writes/reads are data races.
14. Testing & Debugging
- Race detector:
go test -race ./...
(orgo run -race .
) -cpu
: run tests on multiple P’sgo test -cpu=1,2,4
pprof
: identify goroutine leaks / blocking profilesgo vet
: basic concurrency lint
15. Patterns That Slap (and when to pick them)
Pattern | When to use |
---|---|
Worker Pool | Many independent tasks; cap concurrency |
Pipeline | Staged processing with backpressure |
Fan-in | Merge results from multiple sources |
Errgroup | Structured tasks where any error cancels all |
Semaphore | Throttle external I/O or CPU-heavy ops |
Ticker/Timer | Heartbeats, 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