Welcome to the Go In-Depth Tour — your one-stop repository for mastering the Go programming language from the ground up to advanced concepts. Whether you're a beginner exploring Go's syntax or an experienced developer diving into low-level memory management and closures, this repo is crafted to help you learn practically, visually, and thoroughly.
This repository includes clear examples and explanations for the following core Go concepts:
- Keywords 🔑
- Operators 🧮
- Variables and Data Types 📦
- Control Statements 🔁
- Functions 🔧
- Scope 🧭
- Closure 🧠
- Struct 🧱
- Array 🔢
- Pointer 🎯
- Slice 🧩
- Map 🗺️
- Contributions
In Go, keywords are reserved words that have special meaning in the language. we can't use them as variable names or identifiers. Go has just 25 keywords that form the building blocks of the language.
Keyword | Purpose |
---|---|
var |
Declares variables |
const |
Declares constants |
type |
Declares a new type |
func |
Declares a function |
package |
Declares the current package name |
import |
Imports packages |
Keyword | Purpose |
---|---|
if |
Starts a conditional block |
else |
Specifies an alternative block |
switch |
Multiple conditional branches |
case |
A branch within switch |
default |
Default branch in switch |
for |
Starts a loop |
range |
Iterates over arrays, slices, maps, etc. |
goto |
Jumps to a label |
fallthrough |
Falls through to next case in switch |
continue |
Skips current loop iteration |
break |
Exits loop or switch |
return |
Exits from a function |
defer |
Delays execution until function ends |
Keyword | Purpose |
---|---|
chan |
Declares a channel |
go |
Starts a goroutine |
select |
Waits on multiple channel ops |
Keyword | Purpose |
---|---|
interface |
Declares an interface type |
struct |
Declares a structure type |
map |
Declares a map type |
Keyword | Keyword | Keyword | Keyword | Keyword |
---|---|---|---|---|
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 |
- Go also reserves some predeclared identifiers like
int
,string
,true
,false
,nil
, etc., which aren't keywords but are important. - Go has no classes, try-catch, or while—this keeps the language simple.
- Case-sensitive: all keywords must be lowercase.
- These identifiers —
append
,cap
,close
,complex
,copy
,delete
,imag
,len
,make
,new
,panic
,print
,println
,real
,recover
— are predeclared built-in functions in Go. - They are not keywords, but they're treated specially by the compiler.
- These functions are part of Go's runtime and are always available without importing any package.
Function | Purpose |
---|---|
append |
Adds elements to the end of a slice and returns the updated slice |
cap |
Returns the capacity of a slice, array, or channel |
close |
Closes a channel |
complex |
Creates a complex number from two float values |
copy |
Copies elements from a source slice to a destination slice |
delete |
Deletes a key-value pair from a map |
imag |
Returns the imaginary part of a complex number |
len |
Returns the length of strings, arrays, slices, maps, or channels |
make |
Allocates and initializes slices, maps, or channels |
new |
Allocates memory for a variable and returns a pointer to it |
panic |
Stops the normal execution of a program (used for critical errors) |
print |
Prints values to the standard output (less commonly used) |
println |
Like print , but adds spaces and a newline |
real |
Returns the real part of a complex number |
recover |
Regains control of a panicking goroutine (used with defer ) |
Operators are special symbols used to perform operations on variables and values. They are categorized as follows:
Operator | Description | Example |
---|---|---|
+ |
Addition | a + b |
- |
Subtraction | a - b |
* |
Multiplication | a * b |
/ |
Division | a / b |
% |
Modulus (Remainder) | a % b |
++ |
Increment (Postfix only) | a++ |
-- |
Decrement (Postfix only) | a-- |
- Go does not support prefix increment/decrement (
++a
,--a
) — only postfix is allowed (a++
,a--
).
Operator | Description | Example |
---|---|---|
== |
Equal to | a == b |
!= |
Not equal to | a != b |
> |
Greater than | a > b |
< |
Less than | a < b |
>= |
Greater than or equal to | a >= b |
<= |
Less than or equal to | a <= b |
- These operators return
true
orfalse
and are commonly used in conditional statements.
Operator | Description | Example |
---|---|---|
&& |
Logical AND | a && b |
|| |
Logical OR | a || b |
! |
Logical NOT | !a |
&&
returnstrue
if both operands aretrue
.||
returnstrue
if at least one operand istrue
.!
inverts the boolean value (i.e.,!true
isfalse
).- Logical expressions are short-circuited:
a && b
stops evaluating ifa
isfalse
a || b
stops evaluating ifa
istrue
Operator | Description | Example |
---|---|---|
& |
Bitwise AND | a & b |
| |
Bitwise OR | a | b |
^ |
Bitwise XOR | a ^ b |
&^ |
Bit clear (AND NOT) | a &^ b |
<< |
Left shift | a << b |
>> |
Right shift | a >> b |
Operator | Description | Example |
---|---|---|
= |
Simple assignment | a = b |
+= |
Add and assign | a += b |
-= |
Subtract and assign | a -= b |
*= |
Multiply and assign | a *= b |
/= |
Divide and assign | a /= b |
%= |
Modulus and assign | a %= b |
&= |
Bitwise AND and assign | a &= b |
|= |
Bitwise OR and assign | a |= b |
^= |
Bitwise XOR and assign | a ^= b |
<<= |
Left shift and assign | a <<= b |
>>= |
Right shift and assign | a >>= b |
Operator | Description | Example |
---|---|---|
& |
Address of | &a |
* |
Pointer dereference | *ptr |
&a
returns the memory address of variablea
.*ptr
accesses the value stored at the memory address held byptr
.
Operator | Description | Example |
---|---|---|
<- |
Send/Receive from a channel | ch <- x , x = <-ch |
ch <- x
sends the valuex
into the channelch
.x = <-ch
receives a value from the channelch
and stores it inx
.
Go is a statically typed, compiled language with a rich set of built-in data types and flexible variable declaration syntax.
var name string // Declaration without initialization (default: "")
var age int = 24 // Declaration with initialization
var height = 165.5 // Type inference (float64)
name := "Ali Akkas" // string
age := 24 // int
height := 165.5 // float64
active := true // bool
var (
name = "ali"
age = 24
)
x, y := 10, 20
Type | Zero Value |
---|---|
int | 0 |
float64 | 0.0 |
string | "" (empty) |
bool | false |
pointer | nil |
slice/map | nil |
Go is statically typed — each variable must have a defined type (either explicitly or inferred).
Type | Description | Example |
---|---|---|
int , int8 , int16 , int32 int64 , uint |
Integer types | var age int = 30 |
float32 , float64 |
Floating points | var price float64 = 9.99 |
string |
Text (UTF-8) | var name string = "Go" |
bool |
Boolean values | var valid bool = true |
complex64 , complex128 |
Complex numbers | var c complex128 = 1 + 2i |
Type | Description | Example |
---|---|---|
array |
Fixed-size sequence of elements | arr := [3]int{1, 2, 3} |
slice |
Dynamic-length version of array | s := []int{1, 2, 3} |
map |
Key-value store | m := map[string]int{"a": 1} |
struct |
Collection of fields (custom type) | type Person struct { Name string; Age int } |
pointer |
Holds memory address of a value | var p *int |
interface |
Defines behavior (methods) | type Shape interface { Area() float64 } |
channel |
Communication between goroutines | ch := make(chan int) |
function |
First-class functions | func add(a, b int) int { return a + b } |
- Use
const
to declare immutable values
const Pi = 3.1415
const Lang = "Go"
- Go is strictly typed — implicit conversion is not allowed.
- Use explicit conversion with the type name.
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// String to bytes and back
str := "hello"
bytes := []byte(str)
newStr := string(bytes)
Control statements are fundamental building blocks that determine the flow of program execution. Control statements in Go allow us to manage the flow of execution within loops and functions. They provide ways to skip iterations, exit loops early, jump to labeled code, and return from functions.
if temp > 5 {
fmt.Println("Condition Satisfied!")
}
if marks > 40 {
fmt.Println("Passed!")
} else {
fmt.Println("Failed!")
}
if marks >= 80 {
grade = "A+"
} else if marks >= 70 {
grade = "A"
} else if marks >= 60 {
grade = "B"
} else if marks >= 40 {
grade = "C"
} else {
grade = "F"
}
Go allows a short statement to execute before the condition.
if err := process(); err != nil {
fmt.Println(err)
}
day := "Friday"
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Friday":
fmt.Println("Weekend coming")
default:
fmt.Println("Weekend!")
}
temperature := 30
switch {
case temperature > 30:
fmt.Println("Hot weather")
case temperature >= 25:
fmt.Println("Room temperature")
default:
fmt.Println("Cold weather")
}
In Go, the fallthrough keyword is used within a switch statement to force execution to continue to the next case, even if the next case expression does not match.
switch num := 1; num {
case 1:
fmt.Println("Case 1")
fallthrough
case 2:
fmt.Println("Case 2")
fallthrough
case 3:
fmt.Println("Case 3")
}
Output:
Case 1
Case 2
Case 3
⚠️ fallthrough ignores the next case's condition and just executes its body.
Syntax:
for init; condition; inc/dec {
---
}
Example:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
Syntax:
for condition {
---
}
Example:
count := 5
for count < 5 {
count++
}
Syntax:
for {
---
break // needed to exit
}
Example:
for {
data := readData()
if data == nil {
break
}
process(data)
}
Syntax:
for index, value := range arr {
---
}
Example:
// Loop on array/slice
arr := []int{1, 2, 3, 4, 5}
for i, val := range arr {
fmt.Printf("%d: %d\n", i, val)
}
// Loop on map
m := map[string]int{"x": 10, "y": 20, "z": 30}
for key, val := range m {
fmt.Printf("%s: %d\n", key, val)
}
// Loop on string
str := "Akkas"
for i, val := range str {
fmt.Printf("%d: %s\n", i, string(val))
}
// Channel
for item := range channel {
process(item)
}
for i := 0; ; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
outer:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if i*j > 4 {
break outer
}
fmt.Println("i:", i, "j:", j)
}
}
func main() {
count := 0
start:
if count < 3 {
fmt.Println("Count:", count)
count++
goto start
}
}
func greet(name string) {
if name == "" {
return
}
}
Statement | Description |
---|---|
break |
Exits current loop or switch |
continue |
Skips current iteration of loop |
goto |
Jumps to a labeled line of code |
return |
Exits the current function |
Label | Named target for goto , break , or continue |
func readFile() {
f, err := os.Open("file.txt")
if err != nil {
return
}
defer f.Close() // Executes when function exits
// Process file
}
- Defers execute in LIFO order
- Arguments are evaluated immediately
- Useful for resource cleanup
- A function is a reusable block of code that performs a specific task.
- A parameter is a variable named in the function definition.
- An argument is the actual value that is passed to the function when it is called.
func functionName(param1 type1, param2 type2) returnType {
// --- function body
return value
}
Example:
func add(a int, b int) int {
return a + b
}
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
func getStats(nums []int) (sum int, count int) {
for _, n := range nums {
sum += n
}
count = len(nums)
return // Automatically returns named values - (sum, count)
}
- A function that calls itself.
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
- An anonymous function is a function that doesn’t have a name.
- It is useful when you want to create an inline function.
- We Can assign an anonymous function to a variable.
add := func(x, y int) int {
return x + y
}
add(2, 3) // 5
func(x, y int) {
sum := x + y
fmt.Println(sum)
}()
- Closure allows functions to remember and access variables from their surrounding lexical scope, even after the outer function has finished executing.
func counter() func() int {
i := 0
op := func() int {
i++
return i
}
return op
}
next := counter()
next() // 1
next() // 2
- Use
defer
to delay execution until the surrounding function returns.
func process() {
defer fmt.Println("Finished!")
fmt.Println("Processing...")
}
Output:
Processing...
Finished!
- We can't call this function, Computer calls this function autometically.
- It will called at the beginning of the program execution (even before main function's called).
✅ Example:
package main
import "fmt"
func init() {
fmt.Println("init function executed")
}
func main() {
fmt.Println("main function executed")
}
🟢 Output:
init function executed
main function executed
Property | Description |
---|---|
Signature | func init() — no parameters, no return value |
Automatic Invocation | Called before main() and after global variables |
Multiple init s |
A package can have multiple init() functions |
File Order | Run in the order files are compiled |
Package Order | Dependencies' init() run before yours |
- A variadic function is a function that accepts a variable number of arguments of the same data type.
- use the ... (ellipsis) syntax before the type to define it.
🧱 Basic Syntax:
func funcName(params ...type) {
// --- function body
return
}
✅ Example:
func variadicFunc(numbers ...int) { // numbers := []int{1, 2, 3, 4, 5}
fmt.Println(numbers, len(numbers), cap(numbers)) // [1, 2, 3, 4, 5], len = 5, cap = 5
}
func sum(nums ...int) int { // nums := []int{1, 2, 3}
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
variadicFunc(1, 2, 3, 4, 5)
summation := sum(1, 2, 3) // 6
summation2 := sum(1, 2, 3, 4, 5) // 15
}
- When we call a variadic function, Go converts the arguments into a slice.
- The variadic parameter is implemented as a slice under the hood.
- The compiler generates code to automatically create this slice.
- Calling with no variadic args creates a nil slice: numbers := []int
- A function that takes another function as a parameter or returns a function as a result or does both is called a higher-order function.
add := func(a, b int) {
sum := a + b
println(sum)
}
func processOperation(a, b int, cb func(x, y int)) func(x, y int) {
// Execute op func
cb(a, b)
return func div(x, y int) {
res := x / y
fmt.Println(res)
}
}
func main() {
result := processOperation(10, 20, add) // Executes: add(10, 20) → prints 30
result(20, 10) // Executes: div(20, 10) → prints 2
}
Benefit | Description |
---|---|
🔄 Reusability | Abstract repeated patterns like filtering, mapping |
🎯 Customizability | Inject behavior as parameters |
⚡ Cleaner Code | Reduce boilerplate with functional patterns |
🔍 Composition | Build complex logic from small reusable functions |
- A receiver function is a function that binds to a type (usually a
struct
) and can be called like a method. ReceiverType
is usually astruct
.- The receiver can be
value
orpointer
.
func (receiver ReceiverType) MethodName(args) ReturnType {
// --- Function body
}
package main
import "fmt"
type User struct {
Name string // Property
Age int
Email string
}
// Receiver Function (receive only User type's variable)
func (user User) printDetails() {
fmt.Println("Name: ", user.Name)
fmt.Println("Age: ", user.Age)
fmt.Println("Email: ", user.Email)
}
func (user User) call(x int) {
fmt.Println("Name: ", user.Name)
fmt.Println("Age: ", user.Age)
fmt.Println("X: ", x)
}
func main() {
user := User{
Name: "Ali",
Age: 24,
Email: "ali@gmail.com",
}
user.printDetails()
user_2 := User{
Name: "Ali Akkas",
Age: 24,
}
user_2.call(30)
}
📍 Address | 📜 Content |
---|---|
0x0000 |
type User struct { Name string Age int Email string } |
0x0100 |
func (user User) printDetails() { fmt.Println("Name:", user.Name) fmt.Println("Age:", user.Age) fmt.Println("Email:", user.Email) } |
0x0200 |
func (user User) call(x int) { fmt.Println("Name:", user.Name) fmt.Println("Age:", user.Age) fmt.Println("X:", x) } |
0x0300 |
func main() { ... } |
📍 Address | 📦 Content |
---|---|
0xFF00 |
user (User struct) • Name: "Ali" (0xA100 )• Age: 24 • Email: "ali@gmail.com" (0xA200 ) |
0xFE00 |
user_2 (User struct) • Name: "Ali Akkas" (0xA300 )• Age: 24 • Email: "" (nil) |
- 🔗 Strings in Go are reference types, stored dynamically on the heap and referenced via pointers.
📍 Address | 🧵 Content |
---|---|
0xA100 |
"Ali" |
0xA200 |
"ali@gmail.com" |
0xA300 |
"Ali Akkas" |
🧪 Operation | 🔍 Details |
---|---|
user.printDetails() |
• Copies user struct from 0xFF00 into a new stack frame.• Accesses Name and Email via heap at 0xA100 and 0xA200 . |
user_2.call(30) |
• Copies user_2 from 0xFE00 to the call frame.• Argument x=30 pushed to stack.• Name resolved from 0xA300 . |
- func (t *Type)
- Works on the original struct (modifiable).
func (usr *User) Birthday() {
usr.Age++ // Modifies the original User data
}
user := User{"Akkas", 24}
person.Birthday()
fmt.Println(person.Age) // 25 (changed)
Receiver functions are Go’s way of attaching behavior to data without full-blown OOP. They enable:
✅ Clean, object-like APIs ✅ Polymorphism via interfaces ✅ Explicit control over mutability
We cannot attach a method to a built-in type like int
. Only custom types (structs, named types) can have methods.
Scope defines the region of code where a variable, function, or other identifier can be accessed.
It determines the visibility and lifetime of program elements, preventing naming conflicts and managing memory efficiently.
There are four primary scopes in Go:
- Block Scope
- Function Scope
- Package Scope
- File Scope
- Variables declared inside
{}
blocks are only visible within that block - Includes control structures (
if
,for
,switch
),functions
, andexplicit blocks
func main() {
x := 10 // Outer scope variable
{
y := 30 // Inner scope variable
fmt.Println(x, y) // Valid (10 30)
}
fmt.Println(y) // Compile error: undefined:y
}
- Variables declared inside functions are visible only within that function.
- Includes parameters and return values.
func calculate(a, b int) (result int) { // a, b, and result are function-scoped
temp := a * b // Also function-scoped
return temp + 10
}
func main() {
result := calculate(10, 20)
fmt.Println(result) // 210
fmt.Println(temp) // Error: undefined:temp
}
⚠️ These variables are re-created every time the function is called.
- Variables declared outside of functions are accessible anywhere in the same package.
- Can be exported (visible to other packages) if capitalized.
package main
var msg = "Package-scoped variable" // Not exported
var Msg = "Package-scoped variable" // Exported
func show() { // Not Exported
fmt.Println(msg) // Works
}
func Add(x, y int) int { // Exported
return x + y
}
func main() {
fmt.Println(msg) // Works
}
✅ Package-scoped variables persist for the lifetime of the program.
⚠️ Can lead to race conditions in concurrent code.
- Imported packages are file-scoped.
package main
import (
"fmt" // Only visible in this file
m "math" // File-scoped alias
)
var _ = secretHelper() // File-scoped initialization
func main() {
fmt.Println(m.Sqrt(4))
}
- When an inner scope variable declares same name as outer scope variable.
x := "Outer-scope variable"
{
x := "Inner-scope variable" // Shadows outer scope variable x
fmt.Println(x) // Inner-scope variable
}
fmt.Println(x) // Outer-scope variable
⚠️ Shadowing can cause bugs — avoid reusing variable names in nested scopes unless intentional.
Lexical scope (also called static scope) is a fundamental concept in Go that determines how and where variables, functions, and other identifiers are accessible based on their physical location in the source code. Unlike dynamic scope (which determines visibility at runtime), lexical scope is determined at compile time.
func outerFunc() func() int {
count := 0 // Lexically-scoped to outerFunc
return func() int {
count++ // The inner function "closes over" count
return count
}
}
func main() {
counter := outerFunc()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2 (count persists)
}
- The inner function maintains a reference to count (not a copy).
- This is possible because Go uses lexical scoping.
Feature | Lexical Scope (Go) | Dynamic Scope (e.g., Bash) |
---|---|---|
🔍 Resolution | At compile time | At runtime |
🧭 Access Rules | Based on code structure | Based on call stack |
⚡ Performance | Faster (resolved early) | Slower (runtime lookup) |
✅ Predictability | More predictable | Less predictable |
package main
import (
"fmt" // Only visible in this file
m "math" // File-scoped alias
)
var global = "package-scope" // package scope
func main() {
local := "function-scope"
if true {
block := "block-scope"
fmt.Println(global) // ✅ Can access global variable
fmt.Println(local) // ✅ Can access local variable
fmt.Println(block) // ✅ Can access block variable
}
fmt.Println(block) // ❌ Not accessible here
val := m.Sqrt(4) // File-scope
fmt.Println(val)
}
Closures are a powerful concept in Go that allow functions to remember and access variables from their surrounding lexical scope, even after the outer function has finished executing. They are often used for:
- Encapsulation (data privacy)
- Function factories (dynamically generating functions)
- Callbacks (event handlers, async operations)
- Stateful functions (maintaining state between calls)
Closures in Go are anonymous functions that capture variables from their surrounding scope. They are useful when we want a function with persistent state.
package main
import "fmt"
const a = 10
var p = 100
func outer() func() {
money := 500
age := 24
show := func() {
money = money + a + p
fmt.Println("Money: ", money)
}
return show
}
func call() {
incr_1 := outer()
incr_1() // 610
incr_1() // 720
fmt.Println("---- Second incremetnt ------")
incr_2 := outer()
incr_2() // 610
incr_2() // 720
}
func main(){
call()
}
func init(){
fmt.Println("==== Bank ====")
}
📜 Address | 📜 Content |
---|---|
0x0000 |
const a = 10 |
0x0100 |
func outer() with local variables and closure |
- money := 500 |
|
- age := 24 |
|
- show := func() { ... } |
|
- return show |
|
0x0200 |
Anonymous closure inside outer() |
- money = money + a + p |
|
- fmt.Println("Money:", money) |
|
0x0300 |
func call() { ... } |
0x0400 |
func main() { call() } |
0x0500 |
func init() |
- fmt.Println("==== Bank ====") |
- Variables captured by a closure are lifted to the heap by the escape analysis of the go compiler.
- Escape analysis is a process performed by the Go compiler to determine whether a variable can be allocated on the stack or must be allocated on the heap
- The
init()
function runs beforemain()
- Prints: ==== Bank ====
- Global variable
p
is initialized in the Data Segment.
Address | Content |
---|---|
0xD000 |
p = 100 |
- A new stack frame is created.
- Local variables are placed on the stack.
Address | Content |
---|---|
0xF100 |
money = 500 |
0xF108 |
age = 24 |
0xF110 |
show (closure) → points to code at 0x0200 |
Closure captures money
and escapes the stack, hence moved to the heap.
Address | Content |
---|---|
0xH100 |
Closure environment (refers to money@0xF100 ) |
- Reads
money
from closure (heap):500
- Reads
a
(constant) from code segment:10
- Reads
p
(global var) from data segment:100
New Value: 500 + 10 + 100 = 610
📍 Heap after update: 0xH100.money = 610
New Value: 610 + 10 + 100 = 720
📍 Heap: 0xH100.money = 720
func newBankAccount(initialBalance float64) (func(float64), func() float64) {
balance := initialBalance // "private" variable
deposit := func(amount float64) {
balance += amount
}
getBalance := func() float64 {
return balance
}
return deposit, getBalance
}
func main() {
deposit, getBalance := newBankAccount(100.0)
deposit(50.0)
fmt.Println(getBalance()) // 150.0
}
balance
moves from stack to heap because it's referenced afternewBankAccount
returns.- Both closures share the same
balance
reference. - The
balance
variable is effectively private, only accessible through the returned functions. balance
is hidden from outside access.
Structs are one of the most important features in Go for organizing and managing data. A struct is a composite data type that groups together fields (variables) under a single name.
type StructName struct {
Field1 datatype1
Field2 datatype2
Field3 datatype3
}
type Person struct {
Name string
Age int
City string
}
p1 := Person{
Name: "Akkas",
Age: 24,
City: "Dhaka",
}
p2 := Person{"Anis", 25, "Chittigong"} // Must follow struct field order
var p3 Person // All fields get their zero values
fmt.Println(p3) // Output: { 0 }
p4 := new(Person) // p4 is a *Person (pointer)
p4.Name = "Ali"
Use the . (dot) operator to access fields.
fmt.Println(p1.Name) // "Akkas"
p1.Age = 30 // Modify a field
Example: Address inside Person
type Address struct {
Street string
Country string
}
type Person struct {
Name string
Age int
Address Address // Nested struct
}
// Initialization
p := Person{
Name: "Ali Akkas",
Age: 24,
Address: Address{
Street: "123 Jatrabari",
Country: "Bangladesh",
},
}
// Accessing nested fields
fmt.Println(p.Address.Country) // "Bangladesh"
- One-time use.
- If we need a struct for a short-lived purpose, we can define it inline.
temp := struct {
ID int
Value string
}{
ID: 1,
Value: "test_value",
}
fmt.Println(temp.Value) // "test_value"
- Structs can be compared only if all fields are comparable.
p1 := Person{"Akkas", 24, "Bangladesh"}
p2 := Person{"Akkas", 24, "Bangladesh"}
fmt.Println(p1 == p2) // true
Structs are the backbone of data organization in Go. They provide:
- ✅ Type safety
- ✅ Flexibility (composition over inheritance)
- ✅ Clean code (group related data)
Arrays in Go are fixed-size, homogeneous (same type) data structures that store elements in contiguous memory. Unlike slices, arrays have a static length that cannot be changed after creation.
var arrayName [length]Type
var arr [3]int // [0, 0, 0]
arr := [3]{1, 2, 3}
arr := [...]{1, 2, 3, 4, 5} // length = 5
arr := [3]int{1, 2, 3}
fmt.Println(arr[1]) // 2
arr[1] = 20 // [1, 20, 3]
// Using for-loop
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
// Using range
for index, value := range arr {
fmt.Printf("%d: %d\n", index, value)
}
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix[1][2]) // 6
func addMatrices(a, b [2][2]int) [2][2]int {
var result [2][2]int
for i := range a {
for j := range a[i] {
result[i][j] = a[i][j] + b[i][j]
}
}
return result
}
Pointers are a fundamental concept in Go that allow us to directly manipulate memory addresses. They are essential for:
- Efficiently passing large data (avoid copying)
- Modifying variables in functions
- Working with data structures (linked lists, trees)
- Interfacing with system-level code
Concept | Description |
---|---|
& operator |
Address-of (gets the address of a value) |
* operator |
Dereference (gets the value at a pointer) |
*Type |
Pointer to a given type |
A pointer is a variable that stores the memory address of another variable.
var ptr *Type // Declares a pointer to a Type
package main
import "fmt"
func print(numbers *[5]int) { // pointer to an array of 5 integers
fmt.Println(numbers) // &[1 2 3 4 5]
}
func main() {
x := 10 // Only value hold | 10
ptr := &x // Address of x | 0xc00000a0e8 | 824633759720
val := *ptr // Value at address ptr | 10
*ptr = 20 // Re-assign the value of x at the address of ptr | 20
fmt.Println("Address of x = ", ptr) // 0xc00000a0e8
fmt.Println("Value at address ptr = ", val) // 10
fmt.Println("Final Value of x = ", x) // 20
// Array print with the help of pointers
arr := [5]int{1, 2, 3, 4, 5}
print(&arr) // pass by reference
}
var p *int
fmt.Println(p == nil) // true
p := new(int) // *int pointing to 0
*p = 42 // Store 42 at the address
type Node struct {
Val int
Next *Node // Pointer to the next node
}
Slices are one of the most powerful and commonly used data structures in Go 🧠
Learn how slices work under the hood, how to use them effectively, and what to avoid.
A slice is a dynamic, flexible view into an array. Unlike arrays, slices can:
- Resize as needed
- Reference only part of the underlying data
- Share data without owning it
type slice struct {
ptr unsafe.Pointer
len int
cap int
}
ptr
: Points to the start of the underlying array segment.len
: Number of elements the slice currently holds.cap
: Total capacity from start pointer to the end of the array.
💡 Slices are reference types — copying a slice doesn't copy the data, just the reference.
slice := arr[start(inclusive):end(exclusive)]
len = end - start
cap = len(arr) - start
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2, 3, 4], len = 3, cap = 4
s1 := s[1:3] // [3, 4], len = 2, cap = 3
- Still points to the same array memory
- Cap is relative to original array
s := []int{1, 2, 3} // [1, 2, 3], len = 3, cap = 3
- Creates slice with a backing array
slice := make([]type, len)
slice := make([]type, len, cap)
s := make([]int, 3) // [0, 0, 0], len = 3, cap = 3
s := make([]int, 3, 5) // [0, 0, 0], len = 3, cap = 5
s[0] = 5 // [5, 0, 0], len = 3, cap = 5
s[2] = 10 // [5, 0, 10], len = 3, cap = 5
s[3] = 20 // runtime error: index out of range [3] with length 3
- First form: len = cap
- Second form: custom capacity
var s []int // [], ptr = nil, len = 0, cap = 0
- No memory allocated for the nil slice
- Common in function return signatures
s := []int{} // [], len = 0, cap = 0
s := make([]int, 0) // [], len = 0, cap = 0
- Yes, memory allocated for the empty slice
- Used in APIs to return empty results
func main() {
var x []int // [], ptr = nil, len = 0, cap = 0
x = append(x, 1) // [1], len = 1, cap = 1
x = append(x, 2) // [1, 2], len = 2, cap = 2
x = append(x, 3) // [1, 2, 3], len = 3, cap = 4
y := x // [1, 2, 3], len = 3, cap = 4 ; new slice (y) created and point to the (x) slice
x = append(x, 4) // [1, 2, 3, 4], len = 4, cap = 4
y = append(y, 5) // [1, 2, 3, 5], len = 4, cap = 4
x[0] = 10
fmt.Println("x = ", x, len(x), cap(x)) // [10, 2, 3, 5], len = 4, cap = 4
fmt.Println("y = ", y, len(y), cap(y)) // [10, 2, 3, 5], len = 4, cap = 4
}
⚠️ Append might create a new array, but if cap is sufficient, both slices share memory!- x and y share memory until append() causes reallocation
Go handles slice growth automatically with these rules:
Condition | Growth Factor |
---|---|
cap ≤ 1024 | ×2 |
cap > 1024 | ×1.25 |
func changesSlice(z []int) []int {
z[0] = 10
z = append(z, 11) // [10, 6, 7, 11], len = 4, cap = 6
return z
}
func main() {
x := []int{1, 2, 3, 4, 5} // [1, 2, 3, 4, 5], len = 5, cap = 5
x = append(x, 6) // [1, 2, 3, 4, 5, 6], len = 6, cap = 10
x = append(x, 7) // [1, 2, 3, 4, 5, 6, 7], len = 7, cap = 10
a := x[4:] // [5, 6, 7], len = 3, cap = 6
y := changesSlice(a) // [10, 6, 7, 11], len = 4, cap = 6
fmt.Println("x = ", x, len(x), cap(x)) // [1, 2, 3, 4, 10, 6, 7], len = 7, cap = 10
fmt.Println("y = ", y, len(y), cap(y)) // [10, 6, 7, 11], len = 4, cap = 6
fmt.Println("x = ", x[0:8], len(x), cap(x)) // [1, 2, 3, 4, 10, 6, 7, 11], len = 7, cap = 10
}
- Original array: [1, 2, 3, 4, 10, 6, 7, 11, _, _]
Method | Allocates Memory? | Description |
---|---|---|
arr[start:end] |
No | Slice of existing array |
s[start:end] |
No | Slice of slice (shares memory) |
s []T{} (empty slice) |
Yes | Empty but allocated |
make([]T, len) |
Yes | Allocates with length |
make([]T, len, cap) |
Yes | Allocates with custom capacity |
var s []T (nil slice) |
No | No allocation |
A map is an unordered collection of key-value pairs where:
- Each key is unique
- Values are accessed via keys
- Keys must be comparable (
==
operator)
map[KeyType]ValueType
data := make(map[string]int)
data := map[string]int{
"ali": 24,
"anis": 25,
"rakib": 27,
}
m := make(map[string]int) // Initialized but empty
fmt.Println(m == nil) // false
var countries map[string]string
fmt.Println(countries == nil) // true
data["arif"] = 25
data["ali"] = 30
myAge := data["ali"] // 30
delete(data, "ali")
totalData := len(data)
age, exists := data["arif"]
if exists {
fmt.Println("Arif's age is: ", age)
} else {
fmt.Println("Arif's data is not found!")
}
if age, exists := data["arif"]; exists {
fmt.Println(age)
}
original := map[string]int{"x": 1}
ref := original
ref["x"] = 2
fmt.Println(original["x"]) // 2 (changed)
for key, value := range data {
fmt.Println("%s: %s\n", key, value)
}
// Values only
for _, value := range colors {
fmt.Println(value)
}
students := map[string]map[string]int{
"Akkas": {
"Math": 90,
"Physics": 85,
},
"Zidan": {
"Math": 90,
"Chemistry": 87,
},
}
fmt.Println(students["Akkas"]["Math"]) // 90
func wordCount(str string) map[string]int {
words := strings.Fields(str)
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
return counts
}
- Keys must be of a type that supports
==
comparison (e.g., string, int). - Maps are reference types (passed by reference)
- Not safe for concurrent use (use
sync.Map
for concurrency)
Contributions are welcome! Whether you're fixing bugs, improving documentation, or adding new Go examples — your help is appreciated.
- 🍴 Fork the repository
- 📥 Clone your forked repo
- 🛠 Create a new branch (
git checkout -b feature-name
) - 💻 Make your changes
- ✅ Commit your changes (
git commit -m "Add new Go example"
) - 🔄 Push to the branch (
git push origin feature-name
) - 📝 Create a Pull Request
Make sure your code follows Go best practices and is properly documented. All PRs are reviewed before merging.
⭐ Star this repo if you found it helpful — it motivates future improvements!