Go (or Golang) is a statically typed, compiled programming language made by Google. It's popular for its simplicity, speed, and built-in support for concurrency.
This guide covers Go basics with examples so we can easily come back and revise together.
Let's dive into Go—the language powering the cloud!
Variables and Constants
Go requires you to declare variables with specific types, making sure everything is checked at compile time. You can declare them in different ways, with or without initial values.
func main() {
// Variable with type inference
var message = "Hello, Go!"
// Short variable declaration (inside functions only)
count := 42
// Constant declaration
const PI = 3.14
// Zero value (false for bool)
var ready bool
// Print all values
fmt.Println(message, count, PI, ready)
// Explicit type conversion (int to float64)
fmt.Println("Type conversion:", float64(count)/7)
}
Key points
- Variables always have a default _"zero" value:
0
,""
,false
, ornil
_(for pointers, slices, maps, etc.). - Constants are fixed values set at compile time. They can only be numbers, strings, or booleans.
- Go _doesn't do automatic type conversion. _You must convert types explicitly using
Type(value)
to avoid hidden bugs.
Control Structures
Go provides familiar control structures with clean, minimal syntax.
A key design choice is that Go requires curly braces {}
even for single-line blocks, but does not require parentheses ()
around conditions.
Simple for
loop
for i := 0; i < 3; i++ {
fmt.Println("Iteration:", i)
}
for
loop used as a while
loop
counter := 0
for counter < 3 {
fmt.Println("Counter:", counter)
counter++
}
Infinite loop with break
sum := 0
for {
sum++
if sum > 3 {
break
}
fmt.Println("Sum:", sum)
}
if-else
statement with inline initialization
if grade := 85; grade >= 70 {
fmt.Println("You passed with a grade of", grade)
} else if grade >= 60 {
fmt.Println("You barely passed with a grade of", grade)
} else {
fmt.Println("You failed with a grade of", grade)
}
switch
statement
role := "admin"
switch role {
case "guest":
fmt.Println("Limited access granted")
case "user":
fmt.Println("Standard access granted")
case "admin":
fmt.Println("Full access granted")
default:
fmt.Println("No access")
}
switch
with time.Weekday()
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("It's the weekend!")
default:
fmt.Println("It's a weekday.")
}
Key Points
-
for
is the only loop in Go — but it's flexible enough to act like a C-stylefor
,while
, or an infinite loop. -
if
can include setup code before the condition — great for things like error checks. -
switch
is powerful and clean:- No fallthrough by default (no
break
needed). - Cases can be expressions, not just constants.
- One case can match multiple values.
-
switch
without an expression meansswitch true
.
- No fallthrough by default (no
- Go supports
goto
, but use it only when absolutely needed (like breaking out of nested loops).
Data Structures: Arrays and Slices
Go has built-in data structures like arrays and slices.
- An array is a fixed-size collection of elements all of the same type, and its size is part of its type — so you can’t resize it.
- A slice is a more flexible, resizable version of an array, and it's the most commonly used sequence type in Go.
Do slices in Go have different types of elements?
No, slices in Go must contain elements of the same type. If you need mixed types, you can use a slice of empty interfaces ([]interface{}
),
but that's generally avoided unless truly needed.
Arrays
- Declared as:
var scores [4]int
- Assigned as:
scores[0] = 32
- Initialized with values:
grades := [3]int{98, 93, 87}
- Letting the compiler count elements:
colors := [...]string{"red", "green", "blue"}
Slices
- Declared as:
var names []string
- Assigned with values:
names = []string{"Alice", "Bob"}
- Initialized directly:
primes := []int{2, 3, 5, 7}
- Using
...
is not allowed in slice literals (only in arrays) - Appending to a slice:
- Single:
names = append(names, "Charlie")
- Multiple:
numbers = append(numbers, 4, 5)
- Single:
- Slice from an array:
arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4] // [20 30 40]
Key Points
- Arrays in Go are value types — not pointers like in some other languages.
- Assigning an array or passing it to a function results in a full copy of the array.
- Slices hold a reference to the underlying array. Copying a slice points to the same data.
- The
append
function may create a new underlying array if capacity is exceeded. - Slicing (e.g.,
s[1:4]
) creates a new slice pointing to the same array, so changes affect the original. - The zero value of a slice is
nil
, with 0 length and 0 capacity.
Data structures: Maps
Maps in Go are the equivalent of hash tables or dictionaries in other languages. They provide an unordered collection of key-value pairs, where each key is unique.
Nil map
// Nil map
var empty map[string]int
fmt.Println("Empty map:", empty, "Nil?", empty == nil)
Creating and adding data to a map
// Map with make and adding data
scores := make(map[string]int)
scores["Alice"], scores["Bob"], scores["Charlie"] = 92, 85, 79
fmt.Println("Scores:", scores)
Map literal
// Literal map
pop := map[string]int{"New York": 8419000, "Los Angeles": 3980000, "Chicago": 2716000}
fmt.Println("Population:", pop)
Access with existence check
// Access with check
if score, ok := scores["Bob"]; ok {
fmt.Println("Bob's score:", score)
} else {
fmt.Println("Bob's score not found")
}
Delete a key
// Delete a key
delete(scores, "Charlie")
fmt.Println("After deletion:", scores)
Iterating over a map
// Iterate map
fmt.Println("City populations:")
for city, p := range pop {
fmt.Printf("%s: %d\n", city, p)
}
Comparing maps
// Compare maps
c1 := map[string]int{"New York": 8419000, "Los Angeles": 3980000}
c2 := map[string]int{"Los Angeles": 3980000, "New York": 8419000}
fmt.Println("Maps equal?", maps.Equal(c1, c2))
Key points
- Maps are reference types; modifying one variable affects all references.
- The zero value of a map is
nil
. Anil
map causes a runtime panic when adding keys. - Map lookups return two values: the value and a boolean for existence.
value, ok := myMap[key]
- The order of iteration over a map is not guaranteed and can change between executions.
Functions
Functions are first-class citizens in Go. They can be passed as arguments, returned from other functions, and assigned to variables. Go functions can return multiple values, making error handling more explicit.
Basic Call
func greet(name string) string {
return "Hello, " + name
}
greeting := greet("Alice")
Multiple Return Values with Error Handling
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
// Inside main function
result, err := divide(10, 2)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Result:", result)
}
Named return values
// Function Definition
func calculate(a, b int) (sum, diff int) {
sum = a + b
diff = a - b
return
}
// Inside main function
s, d := calculate(5, 3)
Variadic functions
// Function Definition
func sum(values ...int) int {
total := 0
for _, v := range values {
total += v
}
return total
}
// Inside main function
result := sum(1, 2, 3, 4)
Higher-order function
func applyOperation(a, b int, op func(int, int) int) int {
return op(a, b)
}
// Inside main function
add := func(x, y int) int { return x + y }
result := applyOperation(5, 3, add)
Key points
Multiple Return Values: Enables cleaner error handling patterns. The convention is to return the error as the last value.
Named Return Values: Parameters in the return type can be named and used as variables within the function, improving readability, especially for complex functions.
Variadic Functions: The
...
syntax allows a function to accept any number of arguments of a specified type. These arguments are received as a slice inside the function.-
Functions as First-Class Citizens: Functions can be:
- Assigned to variables
- Passed as arguments
- Returned from other functions
- Stored in data structures
Closures: Go supports anonymous functions and closures, which can access variables from their enclosing scope even after that scope has finished executing.
Defer Statement: The
defer
keyword schedules a function call to be executed just before the function returns, useful for cleanup operations.
Closures and Pointers
Closures are anonymous functions that can reference variables from the function in which they are defined. Pointers in Go hold the memory address of a value, allowing you to share data across your program without copying it.
Closures
// Function Definition for Closure
func closureExample() func() int {
x := 10
return func() int {
return x
}
}
// Inside main function
closure := closureExample()
fmt.Println(closure()) // Outputs 10
Pointers
// Function Definition for Pointers
func incrementValue(p *int) {
*p += 1
}
// Inside main function
value := 5
incrementValue(&value)
fmt.Println(value) // Outputs 6
Structs and Custom Types
Structs in Go are collections of fields, similar to classes in other languages but without inheritance. Go also allows you to create custom types based on existing types, with methods as well.
Custom type & method for it
type UserID int
func (id UserID) Str() string {
return fmt.Println(id)
}
var userId UserID = 42
fmt.Println("User ID:", userID)
fmt.Println("Formatted ID:", userID.String())
Basic struct definition & Methods
type Product struct {
ID int
Name string
Price float64
CreatedAt time.Time
}
func NewProduct(id int, name string, price float64) *Product {
return &Product{
ID: id,
Name: name,
Price: price,
CreatedAt: time.Now(),
}
}
func (p Product) PriceWithTax(taxRate float64) float64 {
return p.Price * (1 + taxRate)
}
func (p *Product) ApplyDiscount(percent float64) {
p.Price = p.Price * (1 - percent/100)
}
NewProduct is a constructor function for the Product struct. It initializes a new Product instance with the given values and sets the CreatedAt field to the current time using time.Now().
The Product struct also has two methods:
-** PriceWithTax :** This is a value receiver method, meaning it works on a copy of the Product instance. It_ does not modify the original Product_. It calculates and returns the price after applying a tax rate, but leaves the original Price unchanged.
- ApplyDiscount : This is a pointer receiver method, meaning it works on the actual Product instance and can modify its fields. It applies a discount percentage directly to the Price field, reducing it in place.
Embedded struct
type InventoryItem struct {
Product // Embedded struct (anonymous field)
Quantity int
Location string
}
It means that the InventoryItem struct embeds another struct, Product, without giving it a field name — this is called embedding an anonymous field in Go.
Creating a var of struct
laptop := Product{
ID: 1,
Name: "Laptop",
Price: 999.99,
CreatedAt: time.Now(),
}
fmt.Println("Product:", laptop)
fmt.Println("Laptop name:", laptop.Name)
fmt.Println("Laptop price:", laptop.Price)
Key Points
Value Semantics: Structs are value types—assign or pass them, and you get a copy.
Constructor Functions: Go doesn’t have built-in constructors, but you can make functions like
NewType()
to create structs.Method Receivers: Methods use value receivers (copy) or pointer receivers (can change original).
Embedding: Go has no inheritance. Use embedding to include another struct directly—fields and methods are promoted.
Method Attachment: You can attach methods to any type, not just structs—as long as the type is defined in the same package.
Example:
type UserID int
func (id UserID) String() string {
return fmt.Println(id)
}
var id UserID = 42
id.String() -> correct
Interfaces
Interfaces in Go define a set of method signatures. A type implements an interface by implementing its methods. Unlike many other languages, interface implementation is implicit in Go-there's no _"implements" _keyword.
Define an interface
type Greeter interface {
Greet() string
}
Independent types
// English type
type English struct{}
func (e English) Greet() string {
return "Hello!"
}
// Spanish type
type Spanish struct{}
func (s Spanish) Greet() string {
return "Hola!"
}
Use of interfaces with types
// Function that accepts interface
func SayGreeting(g Greeter) {
fmt.Println(g.Greet())
}
func main() {
eng := English{}
esp := Spanish{}
SayGreeting(eng) // Output: Hello!
SayGreeting(esp) // Output: ¡Hola!
}
Key points
- Interfaces in Go make code flexible and loosely connected.
- Go types don’t need to declare they implement an interface — if they have the required methods, they do.
- The empty interface
interface{}
(orany
in Go 1.18+) has no methods, so all types implement it. Useful for unknown types. - Go encourages small, focused interfaces — often with just one or two methods.
Important points
var w io.Writer // `w` is an interface variable of type Writer
w = os.Stdout // Assign a concrete value of type *os.File to it
w.Write([]byte("Hello")) // Call the Write method through the interface
- io.Writer is an interface that has a method Write([]byte) (int, error)
- os.Stdout is a concrete value of type *os.File, which implements the io.Writer interface.
- So w now holds the type *os.File and its value (os.Stdout).
- You can call w.Write(...) even though you don’t know the exact type at compile time — that’s polymorphism.
Enums
Go doesn't have a dedicated enum type like some other languages, but it provides a pattern using constants and the iota identifier to create enumerated types.
type Month int
const (
January Month = iota + 1 // Start with 1 instead of 0
February
March
April
)
type Color string
const (
Red Color = "RED"
Green Color = "GREEN"
Blue Color = "BLUE"
)
//usage
var color Color = Blue
iota is reset to 0 whenever the const keyword appears and increments by 1 for each constant in a block.
Generics
Generics in Go allow you to write functions and data structures that operate on values of any type, while still maintaining type safety. They're particularly useful for implementing reusable algorithms and data structures.
Generic function with a type parameter
func PrintItems[T any](items []T) {
fmt.Println("Items contents:")
for i, item := range items {
fmt.Printf("[%d]: %v\n", i, item)
}
}
Using interface for constraints
type Numeric interface {
int | int8 | int16 | int32 | int64 | float32 | float64
}
func Sum[T Numeric](values []T) T {
var sum T
for _, v := range values {
sum += v
}
return sum
}
Conclusion
Go is a powerful yet simple language that offers a strict type system, slices, pointers, generics, and interfaces with implicit inheritance.
In the next section, I'll discuss goroutines, wait times, and other related topics, covering the remaining features of Go.
Top comments (1)
worth the read!