If you are not a medium member, you can still read this story here
Part 1: Link
Part 3: Link
In the previous section, we laid the foundation by exploring Go’s basic syntax, data types, and core concepts such as functions, control structures, and basic input/output. This part focuses on essential features such as error handling, pointers, and advanced data structures, including arrays, slices, maps, and structs.
Table of Contents:
1. Pointers in Go
2. Arrays, Slices, Maps, and Structs in Go
3. Type Casting
4. The make Function
5. Iterating with range
6. Type Inference
7. Exports
8. Errors
9. Panic
10. Recover
11. Packages
Pointers in Go
Pointers are a powerful feature in Go that allow you to work with memory addresses directly. They can be used to reference and manipulate the value of a variable without copying the value itself. Understanding pointers is crucial for efficient memory management and implementing certain programming patterns.
What is a Pointer?
A pointer is a variable that stores the memory address of another variable. By using pointers, you can access and modify the value of a variable indirectly.
Declaring Pointers
To declare a pointer, you use the * (asterisk) symbol before the type:
var p *intGetting the Address of a Variable
You use the & (address-of) operator to get the address of a variable:
var x int = 10
var p *int = &xHere, p holds the memory address of x.
Dereferencing Pointers
To access the value at the address stored in a pointer, you use the * (dereference) operator:
var value int = *pIn this example, *p gives you the value stored at the address p is pointing to, which is the value of x.
Modifying Values via Pointers
You can modify the value of a variable through its pointer:
func main() {
var x int = 10
var p *int = &x
// Modify the value of x through the pointer
*p = 20
fmt.Println("x:", x) // Output: x: 20
}Nil Pointers
A pointer that hasn’t been assigned an address is called a nil pointer. It points to zero and should be checked before dereferencing to avoid runtime errors.
var p *int
if p == nil {
fmt.Println("Pointer is nil")
}Passing Pointers to Functions
Passing a pointer to a function allows the function to modify the original variable. This is useful for functions that need to update their input values.
package main
import "fmt"
func increment(p *int) {
*p++
}
func main() {
var x int = 10
increment(&x)
fmt.Println("x after increment:", x) // Output: x after increment: 11
}Arrays, Slices, Maps, and Structs in Go
Arrays, Slices, Maps and Structs are called as complex data types. These data structures provide various ways to organize and manage data in Go.
Arrays
Arrays in Go are fixed-size sequences of elements of the same type. The size of an array is part of its type, meaning arrays with different sizes are considered different types.
Declaring Arrays
You declare an array by specifying its size and type:
var arr [5]int // Array of 5 integers
arr[0] = 10 // Assign value to the first element
arr[1] = 20 // Assign value to the second elementInitializing Arrays
Arrays can be initialized with values:
arr := [3]int{1, 2, 3} // Array with 3 elements initializedArray Length
The length of an array is fixed and determined at compile time. You can get the length using the len function:
arr := [4]int{1, 2, 3, 4}
fmt.Println("Length of array:", len(arr))Slices
Slices are a more flexible and powerful abstraction over arrays. Unlike arrays, slices are dynamically-sized and can grow or shrink as needed.
Declaring and Initializing Slices
Slices are declared and initialized using the [] syntax:
slice := []int{1, 2, 3, 4}Slice Operations
- Appending: Use the
appendfunction to add elements to a slice:
slice := []int{1, 2, 3}
slice = append(slice, 4, 5) // Append values to the slice- Slicing: Create a new slice from an existing slice:
original := []int{1, 2, 3, 4, 5}
sub := original[1:4] // Slice from index 1 to 3Maps
Maps are unordered collections of key-value pairs. Each key is unique, and values are accessed using keys.
Declaring and Initializing Maps
Maps are declared and initialized using the make function or a map literal:
m := map[string]int{
"Alice": 30,
"Bob": 25,
}Accessing and Modifying Maps
You can access and modify map values using the key:
m["Charlie"] = 40 // Add new key-value pair
age := m["Alice"] // Access value by keyChecking Key Existence
To check if a key exists in the map, use the second return value:
func main() {
m := map[string]int{"Alice": 30}
age, exists := m["Bob"]
if exists {
fmt.Println("Bob's age:", age)
} else {
fmt.Println("Bob not found")
}
}Structs
Structs are composite data types that group together variables (fields) under a single name. They are useful for creating custom data types.
Declaring and Initializing Structs
Define a struct type and create instances of it:
// Define a struct type
type Person struct {
Name string
Age int
}
func main() {
// Create an instance of Person
p := Person{Name: "Alice", Age: 30}
fmt.Println("Person:", p)
// Accessing struct fields
fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)
}Struct Literals
You can initialize structs using literals:
// Create and initialize a struct with field names
p1 := Person{Name: "Bob", Age: 25}
// Create and initialize a struct without field names
p2 := Person{"Charlie", 40}Pointers to Structs
You can use pointers to structs to modify struct fields directly:
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
modifyAge(&p)
fmt.Println("Updated Person:", p)
}
func modifyAge(p *Person) {
p.Age = 31 // Modify struct field through pointer
}Type Casting
Type Casting (or type conversion) in Go allows you to convert a value from one type to another. Go is a statically typed language, meaning that types are checked at compile time, and explicit type conversions are required to change a value from one type to another.
Basic Type Conversion
You can convert a value from one type to another using a simple syntax. The conversion must be valid and compatible with the target type.
package main
import "fmt"
func main() {
var intValue int = 42
var floatValue float64 = float64(intValue) // Convert int to float64
fmt.Println("Integer value:", intValue)
fmt.Println("Float value:", floatValue)
}Type Assertion
In Go, type assertion is used to extract the dynamic type of an interface. This is different from type conversion and is often used with interfaces.
package main
import "fmt"
func main() {
var i interface{} = "Hello, World!"
str, ok := i.(string) // Type assertion
if ok {
fmt.Println("String value:", str)
} else {
fmt.Println("Not a string")
}
}Here, i.(string) asserts that i holds a value of type string. The ok variable indicates whether the assertion was successful.
The make Function
In Go, the make function can also be used to create slices, maps, and channels (we’ll cover channels later). It initializes and allocates memory for these data structures and is often used to ensure that the data structures are ready to use.
Syntax:
make(type, size, capacity)type: The type of the data structure (e.g.,[]int,map[string]int,chan int).size: The initial length (for slices and channels) or capacity (for slices).capacity: (Optional) The maximum size of the data structure. For slices, this is the maximum size it can grow to.
Examples:
// Create a slice with length 5 and capacity 10
slice := make([]int, 5, 10)
// Create a map with an initial capacity of 10
m := make(map[string]int, 10)Iterating with range
The range keyword is used to iterate over various data structures in Go. It simplifies the process of accessing elements in arrays, slices, maps, and channels.
Iterating Over Arrays and Slices
When using range with arrays or slices, it returns two values: the index and the value at that index.
arr := [3]int{10, 20, 30}
// Iterate over the array
for index, value := range arr {
fmt.Printf("Index %d: Value %d\n", index, value)
}Iterating Over Maps
When using range with maps, it returns two values: the key and the value associated with that key.
m := map[string]int{"Alice": 30, "Bob": 25}
// Iterate over the map
for key, value := range m {
fmt.Printf("Key %s: Value %d\n", key, value)
}Type Inference
Type Inference in Go allows the compiler to automatically determine the type of a variable based on the assigned value. This feature simplifies code by reducing the need for explicit type declarations.
Variable Declaration with Type Inference
When you use the short variable declaration :=, Go infers the type of the variable from the value on the right-hand side.
func main() {
x := 42 // Type inferred as int
y := 3.14 // Type inferred as float64
name := "Alice" // Type inferred as string
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("name:", name)
}x is inferred to be of type int, y is inferred to be of type float64, and name is inferred to be of type string.
Exports
Exports in Go refer to the visibility of functions, types, and variables outside the package they are defined in. Only identifiers that start with an uppercase letter are exported and accessible from other packages.
Exported Identifiers
- Exported Functions: Functions whose names start with an uppercase letter are exported and can be accessed from other packages.
- Exported Types: Types whose names start with an uppercase letter are exported and can be accessed from other packages.
- Exported Variables: Variables whose names start with an uppercase letter are exported and can be accessed from other packages.
package mathutils// Exported function
func Add(a, b int) int {
return a + b
}
// Non-exported function
func subtract(a, b int) int {
return a - b
}
The Add function is exported and can be accessed from other packages, while the subtract function is not exported and is only available within the mathutils package.
Unexported Identifiers
- Unexported Functions: Functions whose names start with a lowercase letter are not accessible outside the package.
- Unexported Types: Types whose names start with a lowercase letter are not accessible outside the package.
- Unexported Variables: Variables whose names start with a lowercase letter are not accessible outside the package.
package mypackage// Exported type
type MyStruct struct {
Field1 int
}
// Non-exported type
type myPrivateStruct struct {
Field2 int
}
MyStruct is exported and can be used by other packages, while myPrivateStruct is not exported and is only used within the mypackage package.
Errors
Errors in Go are a fundamental concept used for handling unexpected situations in a program. Go’s approach to error handling is straightforward and relies on the explicit return of error values from functions. This approach makes it clear when a function can fail and allows for more predictable error handling.
Error Type
In Go, the error type is a built-in interface used to represent errors. The error interface is defined as follows:
type error interface {
Error() string
}This means any type that implements the Error() method, which returns a string, satisfies the error interface. Most error handling in Go revolves around this interface.
Creating Errors
You can create errors using the errors package, which provides a simple way to create error values.
import "errors"func doSomething(flag bool) error {
if !flag {
return errors.New("something went wrong")
}
return nil
}Here, errors.New creates a new error with the specified message.
You can also define custom error types by implementing the error interface:
type MyError struct {
Msg string
}func (e *MyError) Error() string {
return e.Msg
}func doSomething(flag bool) error {
if !flag {
return &MyError{"something went wrong"}
}
return nil
}Handling Errors
Functions that can fail return an error value as their last return value. It's common practice to check this error value before proceeding.
func main() {
err := doSomething(false)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Success!")
}Panic
Panic is a mechanism in Go for handling unexpected situations that require immediate termination of the program. When a function calls panic, the normal execution of the program stops, and the program begins unwinding the stack, executing any deferred functions.
Triggering Panic
You can trigger a panic using the built-in panic function. This should be used sparingly, as it bypasses normal error handling.
func doSomething(flag bool) {
if !flag {
panic("something went seriously wrong")
}
}Stack Unwinding
When a panic occurs, Go will execute any deferred functions before terminating the program. This is useful for cleanup tasks.
func main() {
defer fmt.Println("Deferred call")
panic("panic occurred")
}This will output the following
Deferred call
panic: panic occurredgoroutine 1 [running]:
main.main()
/path/to/file.go:6 +0x39
exit status 2
Recover
Recover is a built-in function used to regain control after a panic. It’s typically used within a deferred function to handle a panic and prevent the program from terminating.
Using Recover
To use recover, you need to call it inside a deferred function. If recover is called during a panic, it returns the value passed to panic and stops the panic process.
func safeDoSomething(flag bool) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}() if !flag {
panic("something went wrong")
} fmt.Println("Operation completed successfully")
}func main() {
safeDoSomething(false)
fmt.Println("Program continues after recovery")
}This will output,
Recovered from: something went wrong
Program continues after recoveryPackages
Packages in Go are a way to organize and group related code. Each package is a collection of Go source files in the same directory. The package name is declared at the top of each source file and is used to access the functions, types, and variables defined within that package.
Creating a Package
To create a package, you place your Go files in a directory and use the package keyword at the top of each file.
// mathutils.go
package mathutils
// Add adds two integers.
func Add(a, b int) int {
return a + b
}In the example above, mathutils is the package name, and it contains a function Add.
Package Directories
Go uses directories to manage packages. Each directory corresponds to a package, and the package name is derived from the directory name.
/project
/mathutils
mathutils.go
main.goImports
Imports are used to bring in code from other packages. The import statement allows you to use the exported functions, types, and variables from other packages in your own package.
Importing Packages
To import a package, you use the import keyword followed by the package path.
package main
import (
"fmt"
"project/mathutils"
)
func main() {
result := mathutils.Add(2, 3)
fmt.Println("Result:", result)
}In this example, the main package imports the fmt package for formatted I/O and the mathutils package to use its Add function.
Import Paths
- Relative Import Paths: Generally not used in Go. Import paths should be based on the module path.
- Module Paths: Import paths are based on the module path defined in the
go.modfile. For example, if your module is namedexample.com/myapp, you would import packages within it using paths likeexample.com/myapp/mathutils.
That concludes part 2 of the course. These advanced data structures, pointers, packages and error handling will be used in almost every library or application you come across. Hence understanding them is crucial for understanding more complex Go program and writing efficient, maintainable, and robust Go code.
In the next part, we will cover even more advanced concepts like working with interfaces and Go Modules in detail. Until then!




