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
=>false
string
=>""
(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*Pi
is 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
const
for 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 original
Best 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
range
for collections. - Use
switch
instead 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.
&x
gives the address of x,*p
dereferences the pointer.
x := 42
p := &x
fmt.Println(*p) // 42
Mutability
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) // 0
Pointer Arithmetic
- Go does not allow pointer arithmetic like C.
- Only possible via the
unsafe
package (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 characters
len
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 error
Instead:
s2 := "H" + s[1:]
fmt.Println(s2) // Hello
If 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
string
for normal text. - Use
[]byte
for binary data (files, network). - Use
[]rune
when 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 pointer
points 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.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 => 0b10000
Clear a bit to 0
x := uint64(0b11111)
x &^= 1 << 2 // clear 3rd bit => 0b11011
Toggle a bit
x := uint64(0b0101)
x ^= 1 << 0 // => 0b0100
x ^= 1 << 2 // => 0b0000
Bitset 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()
}