1) Variables
Declaration and Initialization
Go is statically typed: the type of every variable is known at compile time. You can declare variables with an explicit type, rely on type inference, or use the short declaration inside functions.
var x int = 10 // explicit type and initialization
var y = 20 // type inferred as int
z := 30 // short declaration (only inside functions)Zero Values
Every variable in Go always has a value. If not explicitly initialized, it gets the zero value for its type:
bool=>falsestring=>""(empty string)- Numeric types (
int,float64,complex128, etc.) =>0(with correct type) rune=>0(represents U+0000)byte=>0(alias foruint8)- Reference types (
slice,map,chan,pointer,interface,func) =>nil
var a int
var s string
var ok bool
fmt.Println(a, s, ok) // 0 "" false| Type | Zero value | Example decl |
|---|---|---|
bool | false | var b bool |
string | "" | var s string |
int / float64 | 0 | var n int / var f float64 |
complex128 | (0+0i) | var c complex128 |
[]T / map[K]V / chan T | nil | var xs []int |
*T (pointer) | nil | var p *int |
interface / func | nil | var r io.Reader / var fn func() |
Zero values at a glance
Constants
Constants represent values known at compile time. They are immutable and don’t occupy memory like variables - the compiler substitutes them directly.
const Pi = 3.14159- Constants are evaluated at compile time.
- The compiler performs constant folding (e.g.
2*Piis computed at compile time).
Untyped vs typed constants
package main
import "fmt"
const u = 5 // untyped int
const f = 5.6 // untyped float
func main() {
var a int = u // ok: u => int
var b float64 = u // ok: u => float64
fmt.Println(a, b) // 5 5
var x float64 = f // ok: f => float64
fmt.Println(x) // 5.6
// var i int = f // error: 5.6 cannot become int
// var i int = int(f) // still error: constants must fit exactly
v := f // bind constant to variable first
var i int = int(v) // explicit conversion (runtime) => 5
fmt.Println(i)
}
Key idea
5 can be int or float64. But lossy conversions like 5.6 => int are never implicit - you must convert explicitly.Compile-time vs Runtime conversions
const f = 5.6; var i int = int(f) => compile error, because 5.6 is not an exact int. Variables: conversions happen at runtime. Example: v := f; var i int = int(v) => works, because now v is a float64 variable and Go allows a lossy runtime conversion (fraction is truncated).Built-in Types & Keywords
Go has a fixed set of basic types and reserved keywords. Knowing them helps avoid confusion and name clashes.
| Category | Examples |
|---|---|
| Boolean | bool |
| Numeric (integers) | int, int8, int16, int32, int64, uint, uint8 (alias byte), uint16, uint32, uint64, uintptr |
| Numeric (floating) | float32, float64 |
| Numeric (complex) | complex64, complex128 |
| Text | string, rune (alias for int32) |
| Other built-in types | error |
What about `uintptr`?
uintptr is a low-level integer used for pointer arithmetic and unsafe/system code. Don’t use it as a general number type.Reserved words (cannot be used as identifiers)
break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var.Predeclared identifiers (can be shadowed - but don't)
int, string, len, make, append, copy, close, new, panic, recover, etc., are available in every file. They are not keywords, so you can shadow them with your own variables - but doing so is confusing and discouraged.package main
import "fmt"
func main() {
len := 42
fmt.Println(len) // prints 42
// fmt.Println(len([]int{1,2,3}))
// ^ This would now FAIL because 'len' refers to your variable, not the built-in.
}Avoid shadowing built-ins
Best Practices
- Prefer short
:=for local variables. - Use
constfor values that never change (leverage untyped constants for ergonomic arithmetic). - Keep package-level variables rare; prefer passing values explicitly.
- Do not shadow predeclared identifiers (
len,string,int,append, ...).
2) Functions
Basic Function
func add(a int, b int) int {
return a + b
}Multiple Return Values
Go functions can return more than one value.
func divmod(a, b int) (int, int) {
return a / b, a % b
}Named Return Values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // returns x, y
}Passing by Value vs by Pointer
All arguments in Go are passed by value. To modify, you must pass a pointer.
func increment(x int) {
x++
}
func incrementPtr(x *int) {
*x++
}
a := 5
increment(a) // copy, no effect
incrementPtr(&a) // modifies originalBest Practices
- Keep functions small and focused.
- Use multiple return values for errors (
value, err). - Use pointers when mutation or avoiding copies of large structs is needed.
3) Control Flow
If / Else
if x > 10 {
fmt.Println("big")
} else {
fmt.Println("small")
}• Can include short initialization:
if n := len(s); n > 0 {
fmt.Println("string not empty")
}Switch
- More concise than many ifs.
- No implicit fallthrough - once a case matches, only that case runs and the switch stops, unlike C where execution continues into the next case automatically.
switch day {
case "Mon", "Tue":
fmt.Println("Weekday")
case "Sat", "Sun":
fmt.Println("Weekend")
default:
fmt.Println("Unknown")
}For Loops
Go has only one loop keyword: for.
for i := 0; i < 3; i++ {
fmt.Println(i)
}While-like:
for x < 10 {
x++
}Infinite:
for {
fmt.Println("looping forever")
}Range
Iterates over slices, arrays, maps, strings, channels.
for i, v := range []string{"a", "b", "c"} {
fmt.Println(i, v)
}Over strings, it yields runes (Unicode code points):
for i, r := range "Hello, 世界" {
fmt.Printf("%d: %c\n", i, r)
}Best Practices
- Prefer
rangefor collections. - Use
switchinstead of long if/else chains. - Avoid infinite loops unless you have a clear break condition.
4) Pointers
Basics
- A pointer stores the address of a value.
&xgives the address of x,*pdereferences the pointer.
x := 42
p := &x
fmt.Println(*p) // 42Mutability
Go copies values by default. To mutate, you must pass a pointer.
func setZero(x *int) {
*x = 0
}
a := 5
setZero(&a)
fmt.Println(a) // 0Pointer Arithmetic
- Go does not allow pointer arithmetic like C.
- Only possible via the
unsafepackage (not recommended for production).
Stack vs Heap
- Small, short-lived variables often stay on the stack.
- If a pointer to a local variable escapes, Go allocates it on the heap.
- Detailed analysis in the Memory lecture.
Best Practices
- Use pointers to avoid copying large structs.
- Use pointers when mutation is intended.
- Avoid pointers when immutability is desired.
5) Strings, Bytes, and Runes in Go
Strings
- A string is an immutable sequence of bytes.
- Internally: pointer to the data + length.
- Because strings are immutable, you cannot change them in place.
s := "Hello, 世界"
fmt.Println(len(s)) // 13 (7+3+3) => number of bytes, not characterslen counts bytes, not characters.Bytes
- A byte is just an alias for
uint8(a number 0-255). - If you convert a string to
[]byte, you see the raw UTF-8 encoding.
b := []byte("ABC")
fmt.Println(b) // [65 66 67]- "A" = 65, "B" = 66, "C" = 67 in ASCII/UTF-8.
Example with Unicode:
s := "世界"
fmt.Println([]byte(s)) // [228 184 150 231 149 140]That’s the UTF-8 encoding of the characters.
Runes
- A rune is an alias for
int32. - It represents a Unicode code point (a single logical character).
r := []rune("世界")
fmt.Println(r) // [19990 30028]
fmt.Println(string(r)) // 世界- [19990 30028] are the Unicode numbers for "世" and "界".
Why the Difference Matters
s := "世界"
// As bytes
fmt.Println(len(s)) // 6 => 6 bytes
fmt.Println([]byte(s)) // [228 184 150 231 149 140]
// As runes
fmt.Println(len([]rune(s))) // 2 => 2 characters世界looks like 2 characters, but UTF-8 encodes each with 3 bytes.- Strings in Go care about bytes, not characters.
Iterating Over Strings in Go
Strings in Go are UTF-8 encoded byte sequences. That means a character (rune) may take 1-4 bytes. How you iterate depends on whether you want raw bytes or Unicode characters (runes).
By Bytes
Indexing a string directly (s[i]) gives you one byte (0-255).
This is fine for ASCII but wrong for Unicode characters.
s := "世界"
for i := 0; i < len(s); i++ {
fmt.Printf("Byte %d = %x\n", i, s[i])
}Output: Byte 0 = e4 Byte 1 = b8 Byte 2 = 96 Byte 3 = e7 Byte 4 = 95 Byte 5 = 8c
"世" is split into 3 bytes: e4 b8 96.
So byte iteration = raw encoding, not characters.By Runes (Idiomatic with `range`)
The range keyword automatically decodes UTF-8 into runes (Unicode code points).
s := "世界"
for i, r := range s {
fmt.Printf("Index %d: Rune %c (U+%04X)\n", i, r, r)
}Output:
Index 0: Rune 世 (U+4E16)
Index 3: Rune 界 (U+754C)
Correct Unicode iteration.
Note how "界" starts at index 3 because "世" used 3 bytes.
By Runes (Manual Decoding with `utf8.DecodeRuneInString`)
For fine control (skipping, peeking, error handling), you can manually decode.
import "unicode/utf8"
s := "世界"
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("Byte %d: Rune %c (U+%04X), size=%d\n", i, r, r, size)
i += size
}Output:
Byte 0: Rune 世 (U+4E16), size=3
Byte 3: Rune 界 (U+754C), size=3
Same result as range, but with explicit size handling.
By Converting to []rune
Convert the string into a rune slice for easy random access by character index.
s := "世界"
runes := []rune(s)
for i, r := range runes {
fmt.Printf("Rune index %d: %c (U+%04X)\n", i, r, r)
}Output: Rune index 0: 世 (U+4E16) Rune index 1: 界 (U+754C) Indexes now count characters, not bytes. Requires allocation (new slice in memory).
Strings Are Immutable
s := "hello"
s[0] = 'H' // compile errorInstead:
s2 := "H" + s[1:]
fmt.Println(s2) // HelloIf you need mutability => convert to []byte or []rune.
s := "世界"
b := []byte(s)
b[0] = 0x41 // 'A'
fmt.Println(string(b)) // A界 (broken! messed up UTF-8)
r := []rune(s)
r[0] = 'A'
fmt.Println(string(r)) // A界 (works correctly)Best Practices
- Use
stringfor normal text. - Use
[]bytefor binary data (files, network). - Use
[]runewhen you care about characters.
len(string) = byteslen([]rune(string)) = characters6) Strings Advanced
Internally, a Go string is just:
- A pointer to the underlying byte array
- A length (number of bytes, not characters)
In the runtime, this is represented by reflect.StringHeader:
type StringHeader struct {
Data uintptr // pointer to the actual bytes
Len int // length in bytes
}Example: Inspecting a String
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
s := "Hello, 世界"
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("Data pointer:", hdr.Data)
fmt.Println("Length:", hdr.Len)
fmt.Println("Bytes:", []byte(s))
}- Data pointer: 4966115
- Length: 13
- Bytes: [72 101 108 108 111 44 32 228 184 150 231 149 140]
- The
Data pointerpoints to the immutable byte array holding those 13 bytes.
Example: reversing a string
package main
import (
"fmt"
)
func rev(s string) string {
newStr := []rune(s)
n := len(newStr)
for i := 0; i < n/2; i++ {
newStr[i], newStr[n-1-i] = newStr[n-1-i], newStr[i]
}
return string(newStr) // convert the rune slice back into a string
}
func main() {
str := "abcde世界"
str2 := rev(str)
fmt.Println(str2) // prints: 界世edcba
}7) Structs
Basics
A struct groups related fields.
type Person struct {
Name string
Age int
}Instantiation
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name)Methods
- Functions with a receiver.
- Under the hood, methods are just functions where the receiver is the first parameter.
func (p *Person) Birthday() {
p.Age++
}Equivalent to:
func Birthday(p *Person) {
p.Age++
}Value vs Pointer Receiver
- Value receiver => works on a copy.
- Pointer receiver => works on the original.
Composition (No Inheritance)
Go does not have inheritance like traditional OOP. Instead, it uses composition: embedding one struct inside another. Methods of the embedded struct get promoted.
package main
import "fmt"
type Address struct {
City, Country string
}
type Person struct {
Name string
Address // embedded (composition)
}
func main() {
p := Person{
Name: "Alice",
Address: Address{City: "Paris", Country: "France"},
}
fmt.Println(p.Name, "lives in", p.City, p.Country) // City and Country promoted
}
8) Interfaces
Basics
An interface defines a set of method signatures. A type satisfies an interface if it has those methods (implicit implementation).
package main
import "fmt"
type Describer interface {
Describe() string
}
type Person struct {
Name string
}
func (p Person) Describe() string {
return "Person: " + p.Name
}
func Print(d Describer) {
fmt.Println(d.Describe())
}
func main() {
p := Person{Name: "Alice"}
Print(p) // works because Person has Describe()
}Key Points
- Implementation is implicit - no 'implements' keyword.
- Interfaces are satisfied automatically if methods match.
- Common for decoupling code and mocking in tests.
Interface Composition
Interfaces can be composed by embedding other interfaces. This is how Go builds larger behaviors from smaller ones (just like struct composition).
package main
import (
"fmt"
)
// Define two small interfaces
type MyReader interface {
Read() string
}
type MyWriter interface {
Write(s string)
}
// Compose them into a bigger interface
type MyReaderWriter interface {
MyReader
MyWriter
}
// A type that implements both Read and Write
type MemoryBuffer struct {
data string
}
func (m *MemoryBuffer) Read() string {
return m.data
}
func (m *MemoryBuffer) Write(s string) {
m.data = s
}
func main() {
var rw MyReaderWriter = &MemoryBuffer{}
// Use it via the combined interface
rw.Write("Hello Interfaces")
fmt.Println("Read back:", rw.Read())
}
io.ReadWriter is just io.Reader + io.Writer. If a type implements both, it implements io.ReadWriter automatically.Type assertions & type switches
A type assertion checks and extracts the dynamic type of an interface value. Use x.(T) to assert that the interface x holds a T. The comma-ok form v, ok := x.(T) avoids panics by returning ok=false if it doesn’t match.
package main
import (
"bytes"
"fmt"
"io"
)
type P struct{}
func (p *P) Write([]byte) (int, error) { return 0, nil }
func main() {
// 1) Basic assertion to a concrete type
var r io.Reader = bytes.NewBufferString("hi")
if buf, ok := r.(*bytes.Buffer); ok {
fmt.Println("len:", buf.Len()) // 2
} else {
fmt.Println("not a *bytes.Buffer")
}
// 2) Assertion to another interface
// bytes.Buffer implements fmt.Stringer (String() string), so this succeeds
if s, ok := any(r).(fmt.Stringer); ok {
fmt.Println("stringer:", s.String()) // "hi"
}
// 3) Type switch inspects the dynamic type
var x any = 42
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case fmt.Stringer:
fmt.Println("stringer:", v.String())
default:
fmt.Printf("other: %T\n", v)
}
// 4) Conversion vs assertion
// You cannot convert from 'any' directly: need assertion first
var y any = int32(5)
// z := int64(y) // compile ERROR
z := int64(y.(int32)) // assert then convert
fmt.Println("z:", z)
// 5) Method-set gotcha (pointer receiver)
var w1 io.Writer = &P{} // *P has Write => implements io.Writer
_ = w1
// var w2 io.Writer = P{} // compile ERROR: P (non-pointer) lacks Write with value receiver
// 6) Nil interface pitfall
var w io.Writer // nil interface: type=nil, value=nil
fmt.Println("w == nil:", w == nil) // true
var nb *bytes.Buffer = nil
var w2 io.Writer = nb // dynamic type=*bytes.Buffer, value=nil
fmt.Println("w2 == nil:", w2 == nil) // false (type is non-nil)
}
Assertion vs conversion
x.(T) is a type assertion from an interface to a concrete (or interface) type. A conversion like int64(v) changes representation between concrete types. From any/interface you must assert first, then convert if needed.Use comma-ok to avoid panics
v, ok := x.(T) and handle !ok. The bare form v := x.(T) panics if x doesn't hold a T.Nil interface pitfall
var w io.Writer compares equal to nil because both dynamic type and value are nil. But if you assign a typed nil (e.g., (*bytes.Buffer)(nil)), the interface is non-nil (type is set), so w != nil.9) Immutability
Value Types
- Examples:
int,float,bool,struct - These are copied on assignment or function call.
- Each copy is independent => changing one does not affect the other.
package main
import "fmt"
func mutateValue(x int) {
x = 100 // local copy only
}
func main() {
a := 42
mutateValue(a)
fmt.Println(a) // 42 => unchanged
}With Structs
type Point struct {
X, Y int
}
func move(p Point) {
p.X = 10
}
func main() {
p1 := Point{1, 2}
move(p1)
fmt.Println(p1) // {1 2}, original not changed
}Best practice: Passing large structs by pointer is more efficient if you need to mutate them.
Pointers and Mutability
If you pass a pointer, the function can modify the original value.
func mutatePointer(x *int) {
*x = 100
}
func main() {
a := 42
mutatePointer(&a)
fmt.Println(a) // 100 => changed!
}Best practice: Use pointers only when mutation is intended and clear. Otherwise, prefer values for safety and clarity.
Strings (Special Case)
- Strings in Go are immutable sequences of bytes.
- You cannot mutate them directly.
func main() {
s := "hello"
// s[0] = 'H' // compile error: cannot assign to s[0]
// Instead: create a new string
s2 := "H" + s[1:]
fmt.Println(s2) // Hello
}Best practice: Treat strings as immutable.
If you need mutability, convert to []byte or []rune
Key Takeaways
| Category | Examples | Behavior | Mutable? |
|---|---|---|---|
| Value types | int, float, bool, struct | Copied | Immutable by copy |
| String | string | Reference-like | Immutable |
| Pointer | *T | Refers to original | Mutable |
- Value types are copied, so functions work on their own copy.
- Pointers let functions mutate the original value.
- Strings are immutable - always create new strings for modifications.
10) Bitwise Operators
What & why
Bitwise operations work directly on the individual bits of an integer. They’re often used for things like flags, permissions, fast math with powers of two and so on.
Operators at a glance
| Operator | Name | Effect | Example |
|---|---|---|---|
& | AND | 1 only if both bits are 1 | 0b1100 & 0b1010 = 0b1000 |
| | OR | 1 if any bit is 1 | 0b1100 | 0b1010 = 0b1110 |
^ (binary) | XOR | 1 if bits differ | 0b1100 ^ 0b1010 = 0b0110 |
^x (unary) | NOT (complement) | inverts every bit | ^0b1010 = ...11110101 |
&^ | AND-NOT (bit clear) | clear bits present in right operand | 0b1100 &^ 0b1010 = 0b0100 |
<< | Left shift | move bits left, fill with zeros | 1 << 3 = 0b1000 |
>> | Right shift | move bits right; unsigned fills with zeros | 8 >> 2 = 0b0010 |
Reading and writing bits
Test a bit
package main
import "fmt"
func IsSet(x uint64, bit uint) bool {
return x&(1<<bit) != 0
}
func main() {
var x uint64 = 0b10100
fmt.Println(IsSet(x, 2)) // true (bit 2 => 4's place)
fmt.Println(IsSet(x, 0)) // false
}
Set a bit to 1
x := uint64(0)
x |= 1 << 4 // set 5th bit => 0b10000Clear a bit to 0
x := uint64(0b11111)
x &^= 1 << 2 // clear 3rd bit => 0b11011Toggle a bit
x := uint64(0b0101)
x ^= 1 << 0 // => 0b0100
x ^= 1 << 2 // => 0b0000Bitset in Go
Write a data structure DynamicSet that represents a set of integers in the range [0, N]. The set must support the following operations efficiently:
Add(x)- insert an elementRemove(x)- delete an elementToggle(x)- flip presence of an elementContains(x)- check if an element is presentClearAll()- remove all elementsSetAll()- include all elements in [0, N]Count()- return the number of elementsForEach(fn)- iterate over elementsUnion(a, b)- produce a new set with all elements from bothIntersection(a, b)- produce a new set with elements common to both
package main
import (
"fmt"
"math/bits"
"strings"
)
type DynamicSet struct {
N uint
buckets []uint8
}
const bitsPerBucket uint = 8
func bucketsFor(n uint) int {
totalBits := n + 1
return int((totalBits + bitsPerBucket - 1) / bitsPerBucket)
}
func New(N uint) *DynamicSet {
return &DynamicSet{
N: N,
buckets: make([]uint8, bucketsFor(N)),
}
}
func (s *DynamicSet) inRange(x uint) bool { return x <= s.N }
func (s *DynamicSet) idx(x uint) (int, uint) {
return int(x / bitsPerBucket), x % bitsPerBucket
}
func (s *DynamicSet) Add(x uint) {
if !s.inRange(x) {
return
}
i, b := s.idx(x)
s.buckets[i] |= uint8(1) << b
}
func (s *DynamicSet) Remove(x uint) {
if !s.inRange(x) {
return
}
i, b := s.idx(x)
s.buckets[i] &^= uint8(1) << b
}
func (s *DynamicSet) Toggle(x uint) {
if !s.inRange(x) {
return
}
i, b := s.idx(x)
s.buckets[i] ^= uint8(1) << b
}
func (s *DynamicSet) Contains(x uint) bool {
if !s.inRange(x) {
return false
}
i, b := s.idx(x)
return s.buckets[i]&(uint8(1)<<b) != 0
}
func (s *DynamicSet) ClearAll() {
for i := range s.buckets {
s.buckets[i] = 0
}
}
func (s *DynamicSet) SetAll() {
for i := range s.buckets {
s.buckets[i] = 0xFF
}
// Trim bits beyond N
last := len(s.buckets) - 1
high := (s.N % bitsPerBucket) + 1
if high < bitsPerBucket {
s.buckets[last] &= (1 << high) - 1
}
}
func (s *DynamicSet) Count() int {
sum := 0
for _, b := range s.buckets {
sum += bits.OnesCount8(b)
}
return sum
}
func (s *DynamicSet) ForEach(fn func(x uint)) {
for x := uint(0); x <= s.N; x++ {
if s.Contains(x) {
fn(x)
}
}
}
func Union(a, b *DynamicSet) *DynamicSet {
maxN := a.N
if b.N > maxN {
maxN = b.N
}
res := New(maxN)
minBuckets := len(a.buckets)
if len(b.buckets) < minBuckets {
minBuckets = len(b.buckets)
}
for i := 0; i < minBuckets; i++ {
res.buckets[i] = a.buckets[i] | b.buckets[i]
}
if len(a.buckets) > len(b.buckets) {
copy(res.buckets[minBuckets:], a.buckets[minBuckets:])
} else if len(b.buckets) > len(a.buckets) {
copy(res.buckets[minBuckets:], b.buckets[minBuckets:])
}
return res
}
func Intersection(a, b *DynamicSet) *DynamicSet {
minN := a.N
if b.N < minN {
minN = b.N
}
res := New(minN)
minBuckets := len(a.buckets)
if len(b.buckets) < minBuckets {
minBuckets = len(b.buckets)
}
for i := 0; i < minBuckets; i++ {
res.buckets[i] = a.buckets[i] & b.buckets[i]
}
return res
}
func (s *DynamicSet) String() string {
var b strings.Builder
b.WriteByte('{')
for x := uint(0); x <= s.N; x++ {
if s.Contains(x) {
fmt.Fprintf(&b, "%d ", x)
}
}
b.WriteByte('}')
return b.String()
}
func main() {
s := New(20)
s.Add(2)
s.Add(5)
s.Toggle(7)
fmt.Println("s:", s) // {2 5 7 }
fmt.Println("count:", s.Count()) // e.g., 3
fmt.Println("has 5?", s.Contains(5)) // true
s.Remove(5)
fmt.Println("has 5?", s.Contains(5)) // false
a := New(12)
b := New(18)
a.Add(1)
a.Add(3)
a.Add(5)
b.Add(3)
b.Add(4)
b.Add(10)
b.Add(18)
u := Union(a, b)
i := Intersection(a, b)
fmt.Println("a:", a) // {1 3 5 }
fmt.Println("b:", b) // {3 4 10 18 }
fmt.Println("a ∪ b:", u)
fmt.Println("a ∩ b:", i)
fmt.Print("iterate: ")
i.ForEach(func(x uint) { fmt.Printf("%d ", x) })
fmt.Println()
}