Golang refresher
Go, also known as Golang, was invented by Robert Griesemer, Rob Pike, and Ken Thompson at Google. The development of Go began in 2007, and the language was officially announced to the public in November 2009.
Go was created to address the shortcomings of existing languages and meet the needs of modern software development. Its focus on simplicity, efficiency, concurrency, and scalability has made it a popular choice for building everything from small utilities to large-scale distributed systems. By solving real-world problems faced by developers, Go has become a key player in the programming language landscape.
Golang vs C++
Basic Syntax
Package Declaration
Every Go file belongs to a package. Packages help organize code and avoid naming conflicts.
// In a file named `main.go`
package main
// In a file named `greetings.go` (inside a `mypackage` directory)
package mypackage
Import Statements
To use code from other packages (standard library or third-party). Following is syntax
import "fmt" // Single import
import (
"fmt"
"math"
"github.com/someuser/somepackage"
) // Multiple imports
Following are few example.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("The time is", time.Now())
}
Alias for Import Statements
We can use alias for better code readability and avoid same name collision.
Without alias
"k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"k8s.io/apimachinery/pkg/apis/meta/v1/client"
////
objectMeta := v1.ObjectMeta{
Name: dnsRecodSetName,
Namespace: projectID,
}
client1 := client.Client // This will throw error collision error
client2 := client.Client // This will throw error collision error
With alias
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrclient "sigs.k8s.io/controller-runtime/pkg/client"
apiclient "k8s.io/apimachinery/pkg/apis/meta/v1/client"
////
objectMeta := metav1.ObjectMeta{
Name: dnsRecodSetName,
Namespace: projectID,
}
client1 := ctrclient.Client
client2 := apiclient.Client
Variables
Following are few example to declare variables.
var age int = 30 // Declare with type and initial value
var name string = "Alice" // Type is inferred
var isAdult bool // Declared without an initial value, gets the zero value (false for bool)
// Short variable declaration (only inside functions)
city := "New York"
// Multiple declarations
var x, y int = 10, 20
Zero Values Variables declared without a value get a “zero value” based on their type:
0
for numeric types""
(empty string) for stringsfalse
for booleansnil
for pointers, functions, interfaces, slices, channels, and maps
package main
import "fmt"
func main() {
var message string = "Hello, Go!"
count := 10
price := 99.99
isValid := true
fmt.Println(message, count, price, isValid)
}
Constants
To define values that cannot be changed.
const pi float64 = 3.14159
const statusOK = 200
const ( // Grouped constants
StatusOK = 200
StatusNotFound = 404
)
package main
import "fmt"
const daysInWeek = 7
func main() {
fmt.Println("There are", daysInWeek, "days in a week.")
}
Data Types
Common Types:
int
: Integer (e.g.,-3, 0, 10
) - various sizes:int8
,int16
,int32
,int64
uint
: Unsigned integer (e.g., 0, 5, 100) - various sizes:uint8
,uint16
,uint32
,uint64
,uintptr
float32
,float64
: Floating-point numbers (e.g., 3.14, -2.5)string
: Text (e.g., "Hello", "Go")bool
: Boolean (true or false)complex64
,complex128
: Complex numbers (less common)byte
: Alias foruint8
(often used for raw data)rune
: Alias forint32
(represents a Unicode code point)
package main
import "fmt"
func main() {
var age int = 30
var price float64 = 99.99
var name string = "Alice"
var isAdult bool = true
var initial rune = 'J'
fmt.Println(age, price, name, isAdult, initial)
}
Operators
- Arithmetic:
+
,-
,*
,/
,%
(modulo) - Comparison:
==
(equal to),!=
(not equal to),>
(greater than),<
(less than),>=
(greater than or equal to),<=
(less than or equal to) - Logical:
&&
(AND),||
(OR),!
(NOT) - Bitwise:
&
(AND),|
(OR),^
(XOR),&^
(AND NOT),<<
(left shift),>>
(right shift) - Assignment:
=
,+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
package main
import "fmt"
func main() {
a := 10
b := 3
sum := a + b
difference := a - b
product := a * b
quotient := a / b
remainder := a % b
fmt.Println("Sum:", sum) // 13
fmt.Println("Difference:", difference) // 7
fmt.Println("Product:", product) // 30
fmt.Println("Quotient:", quotient) // 3
fmt.Println("Remainder:", remainder) // 1
isEqual := (a == b)
isGreater := (a > b)
fmt.Println("Is equal:", isEqual) // false
fmt.Println("Is greater:", isGreater) // true
}
Formatting in golang
in Go, it is a standard practice to add an extra line break between the package
declaration and the import
statement, as well as between other top-level declarations (e.g., import
, const
, var
, func
, etc.). This is part of Go's official formatting conventions and is enforced by the gofmt
tool, which is the standard tool for formatting Go code.
Here’s an example of a properly formatted Go file:
package main
import "fmt"
const greeting = "Hello, World!"
func main() {
fmt.Println(greeting)
}
- Line break after
package
: There is a blank line betweenpackage main
andimport "fmt"
. - Line break after
import
: If there are other top-level declarations (e.g.,const
,var
,func
), they are separated by a blank line.
What Happens If You Don’t Add the Line Break?
If you omit the extra line break, gofmt
(or go fmt
) will automatically add it for you. For example, if you write:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Running gofmt
will reformat it to:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
How to Use gofmt
:
gofmt -w filename.go
gofmt -w .
gofmt -l .
Language type
Go (Golang) is not a loosely typed language. It is a statically typed and strongly typed language. Let me explain what this means and how it differs from loosely typed languages.
Statically Typed
- In Go, the type of a variable is known at compile time.
- You must declare the type of a variable when you define it, or the type is inferred at the time of declaration.
- Once a variable’s type is set, it cannot change.
var x int = 10 // x is explicitly declared as an int
y := 20 // y is inferred as an int
If you try to assign a value of a different type to x
or y
, the compiler will throw an error:
x = "Hello" // Error: cannot use "Hello" (type string) as type int
Strongly Typed
- Go enforces strict type rules. You cannot perform operations between incompatible types without explicit type conversion.
- For example, you cannot add an
int
and afloat64
without converting one of them.
var a int = 10
var b float64 = 5.5
// This will cause a compilation error
// result := a + b // Error: invalid operation: a + b (mismatched types int and float64)
// You need to explicitly convert types
result := float64(a) + b
fmt.Println(result) // Output: 15.5
Comparison with Loosely Typed Languages
- Loosely typed languages (e.g., JavaScript, PHP) allow variables to change types dynamically and perform implicit type conversions.
- For example, in JavaScript, you can do this:
let x = 10; // x is a number
x = "Hello"; // x is now a string
let y = x + 5; // y is "Hello5" (implicit type conversion)
- In Go, such behavior is not allowed. Types are fixed and must be explicitly handled.
Type Inference in Go
While Go is statically typed, it does support type inference for shorthand variable declarations (:=
). The compiler infers the type based on the assigned value.
x := 10 // x is inferred as int
y := 3.14 // y is inferred as float64
z := "Hello" // z is inferred as string
However, once the type is inferred, it cannot change:
x = "Hello" // Error: cannot use "Hello" (type string) as type int
Dynamic Typing in Go?
Go does not support dynamic typing. However, it provides some flexibility through interfaces and type assertions:
- Interfaces: Allow you to define behavior without specifying the exact type.
- Type Assertions: Allow you to check and convert types at runtime.
package main
import "fmt"
func printValue(val interface{}) {
switch v := val.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
func main() {
printValue(42) // Output: Integer: 42
printValue("Hello") // Output: String: Hello
printValue(3.14) // Output: Unknown type
}
Control Structures
if
, else if
, else
Statements
To execute different blocks of code based on whether a condition is true or false. Following is syntax.
if condition1 {
// Code to execute if condition1 is true
} else if condition2 {
// Code to execute if condition1 is false and condition2 is true
} else {
// Code to execute if both condition1 and condition2 are false
}
Following is one example.
package main
import "fmt"
func main() {
age := 15
if age < 13 {
fmt.Println("You are a child.")
} else if age >= 13 && age < 20 {
fmt.Println("You are a teenager.")
} else {
fmt.Println("You are an adult.")
}
}
Short Statements: You can include a short statement (like a variable declaration) before the condition, which is often used for error handling:
Syntax
if short_statement; condition {
// Code to execute if condition is true
} else {
// Code to execute if condition is false
}
- The
short_statement
is executed before evaluating thecondition
. - Variables declared in the
short_statement
are only accessible within theif
orelse
block.
Following is example.
if err := someFunction(); err != nil {
// Handle the error
} else {
// Proceed if there was no error
}
// err is not accessible here.
Using Multiple Short Statements: You can use multiple short statements separated by commas.
package main
import "fmt"
func main() {
if x, y := 10, 20; x < y {
fmt.Println("x is less than y") // Output: x is less than y
} else {
fmt.Println("x is greater than or equal to y")
}
}
Error handling
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
if result, err := divide(10, 2); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result) // Output: Result: 5
}
if result, err := divide(10, 0); err != nil {
fmt.Println("Error:", err) // Output: Error: division by zero
} else {
fmt.Println("Result:", result)
}
}
switch
Statements
To select one of many code blocks to execute, similar to a series of if
/else if
but often more concise and readable, especially when dealing with multiple possibilities.
Syntax
switch expression { // The expression is optional
case value1:
// Code to execute if expression == value1
case value2, value3: // Multiple values can be matched
// Code to execute if expression == value2 or expression == value3
default: // Optional
// Code to execute if no other case matches
}
Example of basic switch case
package main
import "fmt"
func main() {
day := "Friday"
switch day {
case "Monday":
fmt.Println("Start of the work week.")
case "Tuesday":
fmt.Println("Taco Tuesday!")
case "Wednesday":
fmt.Println("Hump day.")
case "Thursday":
fmt.Println("Almost there...")
case "Friday":
fmt.Println("TGIF!")
default:
fmt.Println("It's the weekend!")
}
}
switch
without an expression (like an if
/else if
/else
chain):
package main
import "fmt"
func main() {
score := 85
switch {
case score >= 90:
fmt.Println("Grade: A")
case score >= 80:
fmt.Println("Grade: B")
case score >= 70:
fmt.Println("Grade: C")
case score >= 60:
fmt.Println("Grade: D")
default:
fmt.Println("Grade: F")
}
}
switch
with a short statement:
switch num := calculateValue(); { // num is only scoped to the switch
case num < 0:
fmt.Println("Negative")
case num > 0:
fmt.Println("Positive")
default:
fmt.Println("Zero")
}
Fallthrough
In Go, switch
cases do not fall through to the next case by default (unlike C/C++/Java). If you want fallthrough behavior, use the fallthrough
keyword explicitly:
switch day {
case "Monday":
fmt.Println("Ugh.")
fallthrough // Execution will continue to the next case
case "Tuesday":
fmt.Println("At least it's not Monday.")
default:
fmt.Println("Some other day.")
}
for
Loops
Go’s only looping construct. Used for repeating a block of code.
Syntax: Go has only one looping construct, the for
loop, but it's versatile and can be used in different ways.
// 1. The most common form:
for initialization; condition; post {
// Code to execute repeatedly
}
// 2. Equivalent to a while loop in other languages
for condition {
// Code to execute repeatedly
}
// 3. An infinite loop (use break to exit)
for {
// Code to execute repeatedly
}
// 4. Looping over a collection with `range`
for index, value := range collection {
// Code that uses the index and value
}
Traditional for
loop example:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
// Output:
// 0
// 1
// 2
// 3
// 4
while
loop style:
package main
import "fmt"
func main() {
count := 0
for count < 3 {
fmt.Println(count)
count++
}
}
// Output:
// 0
// 1
// 2
Infinite loop (with break
):
package main
import "fmt"
func main() {
i := 0
for { // Infinite loop
fmt.Println(i)
i++
if i >= 5 {
break // Exit the loop when i is 5
}
}
}
Looping with range
:
package main
import "fmt"
func main() {
numbers := []int{2, 4, 6, 8, 10}
// Get both index and value
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// Only get the value (ignore the index using the blank identifier "_")
for _, value := range numbers {
fmt.Println("Value:", value)
}
// Only get the index
for index := range numbers {
fmt.Println("Index:", index)
}
// Looping over a string (get runes/characters)
message := "Hello"
for index, char := range message {
fmt.Printf("Index: %d, Character: %c\n", index, char)
}
}
break
Exits the innermost for
, switch
, or select
statement immediately.
for i := 0; i < 10; i++ {
if i == 5 {
break // Stop the loop when i is 5
}
fmt.Println(i)
}
// Output: 0 1 2 3 4
continue
Skips the rest of the current iteration of the innermost for
loop and proceeds to the next iteration.
for i := 0; i < 5; i++ {
if i == 2 {
continue // Skip the iteration when i is 2
}
fmt.Println(i)
}
// Output: 0 1 3 4
Labels (with break
and continue
): You can use labels to break
or continue
from outer loops when you have nested loops:
outerLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outerLoop // Breaks out of both loops
}
fmt.Println(i, j)
}
}
// Output:
// 0 0
// 0 1
// 0 2
// 1 0
goto
(Use Sparingly!)
Transfers control to a labeled statement within the same function.
goto labelName
// ...
labelName: // Statement
Example
package main
import "fmt"
func main() {
i := 0
loopStart: // Label
if i < 5 {
fmt.Println(i)
i++
goto loopStart // Jump back to the label
}
}
Functions
Fundamentals of Go Functions
What are they? Functions are blocks of reusable code that perform specific tasks. They are essential for organizing code, promoting modularity, and avoiding repetition (DRY — Don’t Repeat Yourself).
Declaration
func functionName(parameter1 type, parameter2 type, ...) returnType {
// Function body (code to be executed)
// ...
return value // If the function has a return type
}
func
keyword: Indicates a function definition.functionName
: A descriptive name for your function (use camelCase).parameters
: A comma-separated list of input values (optional). Each parameter has a name and a type.returnType
: The type of value the function returns (optional). If omitted, the function doesn't return a value.return
statement: Used to return a value from the function. The type of the returned value must match thereturnType
.
Basic Function Example
package main
import "fmt"
// Adds two integers and returns the sum.
func add(x int, y int) int {
return x + y
}
func main() {
result := add(5, 3)
fmt.Println("Sum:", result) // Output: Sum: 8
}
Multiple Parameters of the Same Type:
func add(x, y int) int { // Shorthand when parameters share the same type
return x + y
}
No Parameters:
func greet() {
fmt.Println("Hello!")
}
Variadic Functions: Functions that accept a variable number of arguments. The variadic parameter must be the last one in the list.
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
s1 := sum(1, 2, 3) // Pass individual arguments
s2 := sum(4, 5, 6, 7, 8) // Pass more arguments
nums := []int{10, 20, 30}
s3 := sum(nums...) // Pass a slice using the ... operator to unpack it
fmt.Println(s1, s2, s3) // Output: 6 30 60
}
Multiple Return Values
Go functions can return more than one value. This is often used to return both a result and an error value.
func divide(x, y int) (int, error) {
if y == 0 {
return 0, fmt.Errorf("division by zero")
}
return x / y, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result) // Output: Result: 5
}
_, err = divide(5, 0) // Use _ to ignore the result and only check for the error
if err != nil {
fmt.Println("Error:", err) // Output: Error: division by zero
}
}
Named Return Values(naked return statement): You can give names to the return values. This can improve readability, especially for functions with multiple return values. It also allows you to use a “naked” return
statement.
func split(sum int) (x, y int) { // x and y are named return values
x = sum * 4 / 9
y = sum - x
return // "Naked" return - implicitly returns x and y
}
func main() {
a, b := split(17)
fmt.Println(a, b) // Output: 7 10
}
Function as Values and Arguments (First-Class Functions)
Functions as Values: In Go, functions are first-class citizens. You can assign them to variables, pass them as arguments to other functions, and return them from functions.
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func subtract(x, y int) int {
return x - y
}
// apply is a higher-order function that takes a function as an argument
func apply(x, y int, operation func(int, int) int) int {
return operation(x, y)
}
func main() {
// Assign functions to variables
var op func(int, int) int
op = add
fmt.Println(op(2, 3)) // Output: 5
op = subtract
fmt.Println(op(5, 2)) // Output: 3
// Pass functions as arguments
sum := apply(4, 6, add)
difference := apply(8, 3, subtract)
fmt.Println(sum, difference) // Output: 10 5
// Anonymous function (function literal) passed directly
product := apply(3, 4, func(x, y int) int {
return x * y
})
fmt.Println(product) // Output: 12
}
Anonymous Functions (Function Literals): Functions without a name. Often used for short, inline operations or when you need a function only once.
greet := func(name string) {
fmt.Println("Hello,", name)
}
greet("Alice") // Output: Hello, Alice
Closures: Anonymous functions that “remember” values from their surrounding scope, even after the outer function has returned.
func adder() func(int) int {
sum := 0 // sum is "captured" by the inner function
return func(x int) int {
sum += x
return sum
}
}
func main() {
myAdder := adder()
fmt.Println(myAdder(5)) // Output: 5
fmt.Println(myAdder(3)) // Output: 8 (sum is remembered)
fmt.Println(myAdder(10)) // Output: 18
}
Deferred Function Calls
defer
keyword: Schedules a function call to be executed after the surrounding function returns, either normally or through a panic.
Common Uses:
- Cleanup: Closing files, releasing resources, unlocking mutexes.
- Ensuring actions are performed regardless of how the function exits.
- LIFO (Last-In, First-Out) Order: Deferred calls are executed in reverse order of their
defer
statements.
package main
import "fmt"
func main() {
fmt.Println("Starting")
defer fmt.Println("Deferred 1")
defer fmt.Println("Deferred 2")
defer fmt.Println("Deferred 3")
fmt.Println("Ending")
}
// Output:
// Starting
// Ending
// Deferred 3
// Deferred 2
// Deferred 1
Example with File Handling:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensure the file is closed when the function exits
// ... process the file ...
return nil
}
init()
Functions
Special functions within a package that are executed automatically when the package is initialized.
- Cannot be called explicitly.
- No parameters or return values.
- A package can have multiple
init()
functions (even in the same file). They are executed in the order they appear in the source code.
Common Uses:
- Initializing package-level variables.
- Setting up connections to databases or other resources.
- Registering handlers or types.
package mypackage
import "fmt"
var myVariable int
func init() {
fmt.Println("Initializing myVariable")
myVariable = 10
}
func init() {
fmt.Println("Another init function in mypackage")
}
// ... other code in the package ...
Recursion
Base Case: A condition that stops the recursion.
Recursive Step: The function calls itself with a modified version of the input, moving towards the base case.
func factorial(n int) int {
if n == 0 { // Base case
return 1
}
return n * factorial(n-1) // Recursive step
}
Data Structures
Arrays
Ordered, fixed-size collections of elements of the same type. Following are ways to declare it.
var arr1 [5]int // Array of 5 integers, initialized to 0
arr2 := [3]string{"apple", "banana", "cherry"} // Array literal
arr3 := [...]int{1, 2, 3, 4, 5} // Compiler determines the length
- Accessing Elements: Use zero-based indexing:
arr1[0]
,arr2[1]
, etc. - Length:
len(arr1)
returns the number of elements in the array. - Fixed Size: Once declared, the size of an array cannot be changed.
package main
import "fmt"
func main() {
var scores [3]int
scores[0] = 85
scores[1] = 92
scores[2] = 78
fmt.Println(scores) // Output: [85 92 78]
fmt.Println(scores[1]) // Output: 92
fmt.Println(len(scores)) // Output: 3
// Array literal
names := [2]string{"Alice", "Bob"}
fmt.Println(names) // Output: [Alice Bob]
}
Multidimensional Arrays: Arrays of arrays.
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix[1][2]) // Output: 6
Value Type: Arrays are value types. When you assign an array to a new variable or pass it to a function, a copy of the array is created.
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // arr2 is a copy of arr1
arr2[0] = 10
fmt.Println(arr1) // Output: [1 2 3] (arr1 is unchanged)
fmt.Println(arr2) // Output: [10 2 3]
Slices
Dynamically sized, flexible views into an underlying array. Much more commonly used than arrays in Go.
var s1 []int // A nil slice (no underlying array)
s2 := []int{1, 2, 3, 4, 5} // Slice literal
s3 := make([]int, 5) // Creates a slice of length 5, capacity 5, initialized to 0
s4 := make([]int, 0, 10) // Length 0, capacity 10 (pre-allocate space)
Relationship to Arrays: A slice doesn’t store data itself; it describes a portion of an underlying array.
Length and Capacity:
len(s)
: The number of elements in the slice.cap(s)
: The maximum number of elements the slice can hold without reallocation (the size of the underlying array from the slice's starting point).
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5} // Slice literal
fmt.Println(numbers) // Output: [1 2 3 4 5]
fmt.Println(len(numbers)) // Output: 5
fmt.Println(cap(numbers)) // Output: 5
// Slicing (creating sub-slices)
slice1 := numbers[1:4] // From index 1 up to (but not including) index 4
fmt.Println(slice1) // Output: [2 3 4]
fmt.Println(len(slice1)) // Output: 3
fmt.Println(cap(slice1)) // Output: 4 (capacity is from index 1 to the end of the original array)
slice2 := numbers[:3] // From the beginning up to index 3
slice3 := numbers[2:] // From index 2 to the end
slice4 := numbers[:] // A copy of the entire slice
fmt.Println(slice2, slice3, slice4) // Output: [1 2 3] [3 4 5] [1 2 3 4 5]
}
Appending
s := []int{1, 2, 3}
s = append(s, 4) // Append a single element
s = append(s, 5, 6, 7) // Append multiple elements
s2 := []int{8, 9, 10}
s = append(s, s2...) // Append another slice (use ... to expand it)
fmt.Println(s) // Output: [1 2 3 4 5 6 7 8 9 10]
make
vs. new
:
make
: Used to create slices (and maps and channels). It initializes the data structure and returns a ready-to-use value.new
: Allocates memory but doesn't initialize it. It returns a pointer to the zero value of the type. Less commonly used for slices.
Reference Type: Slices are reference types. When you assign a slice to a new variable, both variables point to the same underlying array.
s1 := []int{1, 2, 3}
s2 := s1 // s2 refers to the same underlying array as s1
s2[0] = 10
fmt.Println(s1) // Output: [10 2 3] (s1 is modified because they share the underlying array)
fmt.Println(s2) // Output: [10 2 3]
Copying: Use the copy
function to create an independent copy of a slice:
src := []int{1, 2, 3}
dest := make([]int, len(src))
numCopied := copy(dest, src) // Copies elements from src to dest
fmt.Println(dest, numCopied) // Output: [1 2 3] 3
Maps
Unordered collections of key-value pairs, where keys are unique and used to retrieve values. Similar to dictionaries (Python), hash tables, or associative arrays.
var m1 map[string]int // A nil map (cannot add keys until initialized)
m2 := make(map[string]int) // Creates an empty map using make
m3 := map[string]string{ // Map literal
"name": "Alice",
"city": "New York",
"country": "USA",
}
- Key Type: Can be any comparable type (types that support
==
and!=
operators), such as numbers, strings, booleans, arrays, structs (if their fields are comparable), and pointers. Slices, maps, and functions cannot be keys. - Value Type: Can be any type.
package main
import "fmt"
func main() {
// Create an empty map using make
ages := make(map[string]int)
// Add key-value pairs
ages["Alice"] = 30
ages["Bob"] = 25
ages["Charlie"] = 35
fmt.Println(ages) // Output: map[Alice:30 Bob:25 Charlie:35] (order is not guaranteed)
// Access values using keys
fmt.Println(ages["Alice"]) // Output: 30
// Check for key existence (comma ok idiom)
age, ok := ages["Bob"]
if ok {
fmt.Println("Bob's age:", age) // Output: Bob's age: 25
}
age, ok = ages["David"] // David is not in the map
if !ok {
fmt.Println("David's age not found") // Output: David's age not found
}
// Update a value
ages["Alice"] = 31
// Delete a key-value pair
delete(ages, "Charlie")
// Iterate over a map
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Output (order not guaranteed):
// Alice is 31 years old
// Bob is 25 years old
}
- Zero Value: The zero value of a map type is
nil
. Anil
map behaves like an empty map when reading, but attempts to write to anil
map will cause a runtime panic. Always initialize maps usingmake
before writing to them. - Reference Type: Maps are reference types.
Strings
Sequences of bytes, typically used to represent text. In Go, strings are immutable (cannot be changed after creation).
var str string = "Hello, Go!"
str2 := "Another string"
- UTF-8: Go strings are encoded using UTF-8, which means each character can take 1 to 4 bytes.
- Immutability: You cannot modify individual characters of a string directly (e.g.,
str[0] = 'J'
). To modify a string, you create a new one.
package main
import (
"fmt"
"strings"
)
func main() {
message := "Hello, Go!"
fmt.Println(message) // Output: Hello, Go!
fmt.Println(len(message)) // Output: 10 (number of bytes, not necessarily characters)
fmt.Println(message[0]) // Output: 72 (byte value of 'H')
fmt.Println(string(message[0])) // Output: H (convert byte back to string)
// String concatenation
s1 := "Hello"
s2 := "World"
s3 := s1 + ", " + s2
fmt.Println(s3) // Output: Hello, World
// Substrings (using slicing)
sub := message[0:5] // From index 0 up to (but not including) index 5
fmt.Println(sub) // Output: Hello
// String methods (many available in the strings package)
fmt.Println(strings.ToUpper(message)) // Output: HELLO, GO!
fmt.Println(strings.Contains(message, "Go")) // Output: true
fmt.Println(strings.ReplaceAll(message, "o", "0")) // Output: Hell0, G0!
}
Runes: Go has a rune
type, which is an alias for int32
. It represents a Unicode code point. Use runes when working with individual characters, especially when dealing with non-ASCII text.
str := "नमस्ते" // Hindi greeting
for _, r := range str {
fmt.Printf("%c ", r) // Output: न म स ् त े
}
String Conversions:
string(b)
: Converts a byte slice[]byte
to a string.[]byte(s)
: Converts a strings
to a byte slice.string(r)
: Converts a runer
to a string.[]rune(s)
: Converts a strings
to a rune slice.- strconv Package: Use the
strconv
package to convert strings to numbers and vice-versa (e.g.,strconv.Itoa
,strconv.Atoi
,strconv.ParseFloat
,strconv.FormatFloat
).
Structs — User-Defined Composite Types
Structs are composite data types that group together values (fields) of different types under a single name. They are used to create custom data structures.
type Person struct {
FirstName string
LastName string
Age int
Address struct { // Embedded struct
Street string
City string
Country string
}
}
Following is example.
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
Address Address // Composition using another struct
}
type Address struct {
Street string
City string
Country string
}
func main() {
// Initialize a struct
p1 := Person{
FirstName: "Alice",
LastName: "Smith",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "New York",
Country: "USA",
},
}
// Access fields using dot notation
fmt.Println(p1.FirstName) // Output: Alice
fmt.Println(p1.Address.City) // Output: New York
// Update fields
p1.Age = 31
// Create a struct with some fields initialized (others get zero values)
p2 := Person{FirstName: "Bob"}
fmt.Println(p2) // Output: {Bob 0 { }}
// Pointer to a struct
p3 := &Person{FirstName: "Charlie", LastName: "Brown", Age: 25}
fmt.Println((*p3).FirstName) // Output: Charlie (accessing through a pointer)
fmt.Println(p3.FirstName) // Output: Charlie (shorthand for struct pointers)
p3.Age = 26 // Modifying through a pointer modifies the original struct
}
- Methods: You can associate functions (called methods) with structs to give them behavior.
- Composition (Embedding): A struct can embed another struct, promoting code reuse and a form of inheritance-like behavior.
- Tags: Struct fields can have tags, which are string literals providing metadata. Commonly used for serialization/deserialization (e.g., JSON, XML), ORM mapping, validation, etc.
type User struct {
ID int `json:"id" validate:"required"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omitempty tag means the field will be omitted if it's empty
}
Object oriented concept
Go (Golang) is not a purely object-oriented programming (OOP) language like Java or C++, but it does support some object-oriented concepts in a unique way. Go avoids traditional OOP features like classes and inheritance, instead favoring composition and interfaces. Here’s an overview of how Go implements object-oriented concepts:
Structs Instead of Classes
In Go, structs are used to define custom types that group together fields (attributes). Structs are similar to classes in other languages but do not support methods directly within the struct definition.
type Person struct {
Name string
Age int
}
Methods on Structs
Go allows you to define methods on structs, which are functions with a special receiver argument. The receiver can be a struct or a pointer to a struct.
func (p Person) Greet() string {
return "Hello, my name is " + p.Name
}
func (p *Person) Birthday() {
p.Age++
}
- Value Receiver:
(p Person)
operates on a copy of the struct. - Pointer Receiver:
(p *Person)
operates on the original struct, allowing modifications.
Composition Over Inheritance
Go does not support inheritance. Instead, it encourages composition by embedding structs within other structs. This allows you to reuse and extend functionality.
type Employee struct {
Person // Embedded struct
JobTitle string
}
func main() {
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
JobTitle: "Developer",
}
fmt.Println(emp.Greet()) // Reusing Person's method
}
Interfaces for Polymorphism
Go uses interfaces to achieve polymorphism. An interface defines a set of method signatures, and any type that implements those methods implicitly satisfies the interface.
type Speaker interface {
Speak() string
}
func (p Person) Speak() string {
return "Hi, I'm " + p.Name
}
func Greet(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
p := Person{Name: "Bob"}
Greet(p) // Output: Hi, I'm Bob
}
- Interfaces in Go are implicitly implemented. There is no need to explicitly declare that a type implements an interface.
- This makes Go’s interfaces flexible and decoupled from specific types.
Encapsulation
Go uses naming conventions to control visibility:
- Exported (public): Identifiers starting with an uppercase letter (e.g.,
Name
,Greet
). - Unexported (private): Identifiers starting with a lowercase letter (e.g.,
age
,greet
).
No Constructors
Go does not have constructors, but it is common to use factory functions to initialize structs.
func NewPerson(name string, age int) Person {
return Person{
Name: name,
Age: age,
}
}
func main() {
p := NewPerson("Charlie", 25)
fmt.Println(p)
}
No Generics (Before Go 1.18)
Before Go 1.18, Go did not support generics, which made it challenging to write reusable, type-safe code. However, generics were introduced in Go 1.18, allowing for more flexible and reusable code.
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"a", "b", "c"})
}
No Method Overloading
Go does not support method overloading (defining multiple methods with the same name but different parameters). Instead, you must use unique method names.
Empty Interface for Dynamic Typing
The empty interface (interface{}
) can hold values of any type, similar to Object
in Java or void*
in C++. This is often used for functions that need to handle multiple types.
func PrintValue(v interface{}) {
fmt.Println(v)
}
func main() {
PrintValue(42)
PrintValue("Hello")
}
Embedding Interfaces
Go allows embedding interfaces within other interfaces, enabling you to compose interfaces.
type Reader interface {
Read() string
}
type Writer interface {
Write(string)
}
type ReadWriter interface {
Reader
Writer
}
Error Handling
The Built-in error
Interface
The error
interface is the standard way to represent errors in Go. It's defined as:
type error interface {
Error() string
}
Any type that implements the Error()
method, which returns an error message as a string, satisfies the error
interface.
Creating Errors
errors.New()
: The simplest way to create a basic error value.
import "errors"
err := errors.New("something went wrong")
fmt.Errorf()
: Creates formatted error messages, similar to fmt.Printf()
. You can use the %w
verb to wrap an existing error, adding context (more on this later).
import "fmt"
err := fmt.Errorf("file not found: %s", filename)
Returning Errors from Functions
Functions that can encounter errors return an error
value as their last (or only) return value. A nil
error indicates success.
func divide(x, y int) (int, error) {
if y == 0 {
return 0, fmt.Errorf("division by zero")
}
return x / y, nil // nil indicates no error
}
Handling Errors
The if err != nil
Pattern: The most common way to handle errors is to check if the returned error is not nil
.
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return // Or handle the error in some other way (e.g., log, retry, exit)
}
fmt.Println("Result:", result) // Proceed if no error
Ignoring Errors (Use with Caution!): You can use the blank identifier _
to ignore an error value. Only do this if you are absolutely sure that the error can be safely ignored.
result, _ := divide(10, 2) // I don't care about potential errors here
fmt.Println("Result:", result)
Custom Error Types
Create a struct and implement the Error()
method to satisfy the error
interface.
`package main
import (
"fmt"
"time"
)
// Custom error type with extra information
type MyError struct {
When time.Time
What string
}
// Implement the Error() method for MyError
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}
func doSomething() error {
// ... some operation that might fail ...
return &MyError{ // Return a pointer to MyError
When: time.Now(),
What: "it didn't work",
}
}
func main() {
if err := doSomething(); err != nil {
fmt.Println(err) // The Error() method is called automatically
// Output (with current time): at 2023-10-27 10:30:00.123456 +0000 UTC, it didn't work
// Type assertion to access MyError fields
if myErr, ok := err.(*MyError); ok {
fmt.Println("Error occurred on:", myErr.When)
fmt.Println("Error message:", myErr.What)
}
}
}
Error Wrapping (Go 1.13+)
Adds context to an error while preserving the original error. This creates an error chain that you can traverse.
fmt.Errorf()
with %w
: Use the %w
verb to wrap an error:
err := readConfig() // Assume readConfig() returns an error
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}
errors.Is()
: Checks if an error (or any error in its chain) matches a specific target error.
if errors.Is(err, os.ErrNotExist) {
// Handle file not found error specifically
}
errors.As()
: Checks if an error (or any error in its chain) can be assigned to a specific type. This is useful for extracting custom error types.
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("Custom error occurred:", myErr.What)
}
Unwrapping with errors.Unwrap()
: You can get the next error in the chain using errors.Unwrap()
.
Sentinel Errors
Predefined, exported error variables that represent specific error conditions.
Example: io.EOF
(end of file), sql.ErrNoRows
(no rows found in a database query).
package mypackage
import "errors"
var ErrResourceNotFound = errors.New("resource not found")
func GetResource(id int) (*Resource, error) {
// ...
if notFound {
return nil, ErrResourceNotFound
}
// ...
}
// In another package:
resource, err := mypackage.GetResource(123)
if err != nil {
if errors.Is(err, mypackage.ErrResourceNotFound) {
// Handle resource not found specifically
} else {
// Handle other errors
}
}
Defer, Panic, and Recover
defer
: Used to schedule a function call to be executed when the surrounding function returns (used for cleanup).panic
: Indicates a serious, often unrecoverable error. It stops the normal flow of execution and starts unwinding the stack, calling deferred functions along the way.recover
: Used inside a deferred function to stop a panic and regain control. It returns the value that was passed topanic
.
func safeDivide(x, y int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
result = 0 // Set a default result
}
}()
if y == 0 {
panic("division by zero") // Cause a panic
}
result = x / y
return result, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Output: Error: recovered from panic: division by zero
}
fmt.Println("Result:", result) // Output: Result: 0
}
When to use panic
:
- Unrecoverable errors: Situations where the program cannot continue, like programming errors (e.g., invalid array index, nil pointer dereference).
- During initialization: If essential setup fails (e.g., cannot connect to a database, cannot load a required configuration file).
When to use recover
:
- Preventing crashes: In long-running servers or critical applications where you want to handle panics gracefully and continue operation.
- Isolating failures: In concurrent programs, you might want to recover from a panic in one goroutine without crashing the entire application.
Error Handling in Goroutines:
- Each goroutine is responsible for handling its own errors.
- Use channels to communicate errors from goroutines back to the main goroutine or other parts of the application.
func worker(jobs <-chan int, results chan<- int, errs chan<- error) {
for j := range jobs {
result, err := processJob(j)
if err != nil {
errs <- err // Send the error on the error channel
return // Or continue processing other jobs
}
results <- result
}
}
Best Practices for Error Handling
- Handle Errors Where They Occur: Don’t just propagate errors up the call stack without doing anything. Log, wrap, or handle errors as close as possible to where they originate.
- Provide Context: Wrap errors with
fmt.Errorf("%w")
to add information about what was happening when the error occurred. - Use Custom Error Types: Define your own error types when you need to represent specific error conditions or attach extra data to errors.
- Use Sentinel Errors Sparingly: Only create sentinel errors for a few specific cases that callers are likely to check for explicitly.
- Don’t Panic for Normal Errors: Use
panic
only for truly exceptional situations that indicate a bug in the program. - Document Your Errors: Explain in comments or documentation what errors a function might return and what they mean.
- Log Errors Effectively: Log errors with enough detail to diagnose problems later. Consider including timestamps, error messages, stack traces (using a library like
pkg/errors
in the past, but now you can use%+v
withfmt.Errorf()
), and any relevant contextual information.
fmt.Errorf() vs fmt.Sprintf
Both fmt.Errorf()
and fmt.Sprintf()
are used for formatting strings, but they serve different purposes.
fmt.Sprintf():
Formats a string and returns it as a string
type. When you need to create a formatted string for logging, messages, or any other purpose.
package main
import (
"fmt"
)
func main() {
name := "Alice"
age := 30
// Format a string
message := fmt.Sprintf("%s is %d years old.", name, age)
fmt.Println(message) // Output: Alice is 30 years old.
}
fmt.Errorf()
: Formats a string and returns it as an error
type. When you need to create a custom error message.
package main
import (
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero: cannot divide %f by %f", a, b)
}
return a / b, nil
}
func main() {
_, err := divide(10, 0)
if err != nil {
fmt.Println(err) // Output: division by zero: cannot divide 10.000000 by 0.000000
}
}
Combining fmt.Sprintf()
and fmt.Errorf():
You can use fmt.Sprintf()
to create a formatted string and then wrap it in an error using fmt.Errorf()
.
package main
import (
"fmt"
)
func main() {
name := "Alice"
age := 30
// Create a formatted string
message := fmt.Sprintf("%s is %d years old.", name, age)
// Wrap the formatted string in an error
err := fmt.Errorf("validation error: %s", message)
fmt.Println(err) // Output: validation error: Alice is 30 years old.
}
Pointers
What are Pointers?
Memory Addresses: A pointer is a variable that stores the memory address of another variable. Think of it as a signpost that points to a specific location in your computer’s memory where a value is stored.
Analogy: Imagine your computer’s memory as a long street with houses (memory locations), each having a unique address. A pointer is like a note that contains the address of a particular house. If you have the address (the pointer), you can find the house (the value stored at that address).
Declaration:
var ptr *int // Declares a pointer to an integer
*T
: The type*T
is a pointer to a value of typeT
. So*int
is a pointer to an integer,*string
is a pointer to a string, and so on.- Zero Value: The zero value of a pointer is
nil
, meaning it doesn't point to anything yet.
The Address-of Operator (&
)
The &
operator is used to get the memory address of a variable.
package main
import "fmt"
func main() {
x := 10
ptr := &x // ptr now holds the memory address of x
fmt.Println("Value of x:", x) // Output: Value of x: 10
fmt.Println("Address of x:", &x) // Output: Address of x: 0xc00001a098 (or some other memory address)
fmt.Println("Value of ptr:", ptr) // Output: Value of ptr: 0xc00001a098 (same address as above)
}
The Dereference Operator (*
)
The *
operator is used to access the value stored at the memory address held by a pointer. This is called dereferencing the pointer.
package main
import "fmt"
func main() {
x := 10
ptr := &x
fmt.Println("Value of ptr:", ptr) // Output: Value of ptr: 0xc00001a098 (memory address)
fmt.Println("Value at ptr:", *ptr) // Output: Value at ptr: 10 (value stored at the address)
*ptr = 20 // Modify the value at the address pointed to by ptr
fmt.Println("New value of x:", x) // Output: New value of x: 20 (x has been changed through the pointer)
}
Pointers and Functions
Pass by Value (Copies): In Go, function arguments are passed by value. This means that when you pass a variable to a function, a copy of the variable’s value is created inside the function. Any changes made to the parameter inside the function do not affect the original variable.
package main
import "fmt"
func changeValue(val int) {
val = 100
}
func main() {
x := 5
changeValue(x)
fmt.Println(x) // Output: 5 (x is unchanged)
}
Pass by Reference (Using Pointers): To modify a variable inside a function, you need to pass a pointer to that variable. This way, the function can directly access and change the value at the original memory location.
package main
import "fmt"
func changeValue(ptr *int) {
*ptr = 100 // Dereference the pointer to modify the value
}
func main() {
x := 5
changeValue(&x) // Pass the address of x
fmt.Println(x) // Output: 100 (x has been changed)
}
The new
Function
The new
function allocates memory for a new variable of a specified type and returns a pointer to that memory. The allocated memory is initialized to the zero value of the type.
Syntax:
ptr := new(int) // Allocates memory for an int and returns a pointer to it
Example:
package main
import "fmt"
func main() {
ptr := new(int) // ptr is a pointer to an int, initialized to 0
fmt.Println("Value of ptr:", ptr) // Output: Memory address (e.g., 0xc00001a0b0)
fmt.Println("Value at ptr:", *ptr) // Output: 0 (zero value of int)
*ptr = 50
fmt.Println("New value at ptr:", *ptr) // Output: 50
}
Pointers and Data Structures
Structs: Pointers are often used with structs to avoid copying large structs and to allow methods to modify the struct’s fields.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p *Person) celebrateBirthday() { // Pointer receiver
p.Age++ // Modifies the original struct
}
func main() {
p := &Person{Name: "Alice", Age: 30} // Pointer to a Person struct
fmt.Println(p.Age) // Output: 30
p.celebrateBirthday()
fmt.Println(p.Age) // Output: 31
}
Arrays/Slices: You can use pointers to elements of an array or slice to modify the original elements. When you pass Arrays as parameter to method then it is passed as value. But Slices are passed as reference.
Map: Map is also passed as reference when used as parameter.
Linked Lists, Trees, etc.: Pointers are essential for building dynamic data structures where elements are linked together using memory addresses.
Pointer Arithmetic (Not Directly Supported): Unlike C/C++, Go does not allow direct pointer arithmetic (e.g., ptr + 2
to move to the next element). This is a deliberate design choice to improve memory safety and prevent common errors. To work with sequences of elements, use slices, which provide bounds checking and prevent out-of-bounds memory access.
When to Use Pointers
- Modifying Values in Functions: When you want a function to be able to modify its arguments.
- Large Data Structures: To avoid copying large amounts of data when passing them around or storing them in data structures.
- Optional Values: A pointer can be
nil
, which can be used to represent an optional value (similar to nullable types in other languages). - Dynamic Data Structures: When building structures like linked lists, trees, graphs, etc.
- Representing Resource Handles: Sometimes pointers are used to represent handles to external resources (e.g., files, network connections), but often interfaces are used for this purpose as well.
Potential Pitfalls
Nil Pointer Dereferences: If you try to dereference a nil
pointer (a pointer that doesn't point to anything), you'll get a runtime panic (similar to a segmentation fault in C/C++). Always check if a pointer is nil
before dereferencing it if there's a possibility that it might not be initialized.
var ptr *int // ptr is nil
if ptr != nil {
fmt.Println(*ptr) // Safe to dereference here
}
Dangling Pointers: A dangling pointer is a pointer that points to memory that has been freed or deallocated. This can happen if you’re not careful about how you manage memory. Go’s garbage collector helps prevent this in most cases, but be aware of the possibility when working with unsafe
code.
Memory Leaks: In Go, memory leaks are less common than in languages without garbage collection. However, you can still create situations where memory is unintentionally held onto and not released. This can happen, for example, if you have long-lived global data structures that keep references to objects that are no longer needed. Be mindful of circular references and long-lived objects.
Concurrency
Concurrency vs. Parallelism
- Concurrency: Dealing with multiple tasks at the same time. It’s about the composition of independently executing processes. Think of a chef juggling various tasks in a kitchen (prepping ingredients, stirring a pot, checking the oven) — they might not all be happening simultaneously, but the chef is managing multiple tasks concurrently.
- Parallelism: Simultaneously executing multiple tasks, typically using multiple CPU cores. It’s about the simultaneous execution of processes. Think of multiple chefs working in a large kitchen, each working on a different dish at the same time.
- Go’s Approach: Go makes it easy to write concurrent programs using goroutines and channels. The Go runtime scheduler then maps these concurrent operations onto the underlying operating system threads, potentially executing them in parallel if multiple cores are available.
Goroutines
Lightweight, independently executing functions. They are managed by the Go runtime, not the operating system. Creating thousands or even millions of goroutines is feasible.
go
keyword: You start a goroutine by simply putting the go
keyword before a function call:
go myFunction() // myFunction will execute concurrently
Following is example for reference.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go say("world") // Start a new goroutine
say("hello") // This continues executing concurrently
}
Output: You’ll see “hello” and “world” interleaved in the output because the two say
functions are running concurrently. The exact order might vary on each run.
Key Idea: When you start a goroutine, the Go runtime adds it to a queue of tasks to be executed. The runtime scheduler then picks up these tasks and runs them, potentially switching between them very rapidly. This gives the illusion of parallelism even on a single-core system.
Channels
Definition: Typed conduits used for communication and synchronization between goroutines. They allow one goroutine to send data to another.
Analogy: Think of channels like pipes connecting two goroutines. One goroutine can put data into the pipe (send), and another goroutine can take data out of the pipe (receive).
Declaration:
ch := make(chan int) // Creates an unbuffered channel of type int
ch := make(chan string, 10) // Creates a buffered channel of strings with capacity 10
Send and Receive Operators:
<-
(Left Arrow): Used for both sending and receiving, depending on the context.
ch <- 5 // Send the value 5 on the channel ch
value := <-ch // Receive a value from the channel ch and assign it to value
Blocking Nature:
- Unbuffered Channels: Sends and receives on unbuffered channels block until both a sender and a receiver are ready. This provides inherent synchronization.
- Buffered Channels: Sends on a buffered channel block only when the buffer is full. Receives block only when the buffer is empty.
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // Send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c) // Start goroutines to sum halves of the slice
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // Receive from c (order might vary)
fmt.Println(x, y, x+y) // Output: -5 17 12 (or 17 -5 12)
}
main
Function and Goroutines:
- The
main
function itself runs in a goroutine (the main goroutine). - If the
main
function exits, the entire program terminates, even if other goroutines are still running. - You need to use synchronization mechanisms (like channels or
sync.WaitGroup
) to ensure thatmain
waits for other goroutines to complete if necessary.
Synchronization and Coordination
Closing a Channel:
close(ch)
: Signals that no more values will be sent on the channel.- Receiving from a closed channel does not block and returns the zero value of the channel’s type.
- It’s the sender’s responsibility to close a channel, not the receiver’s.
range
over Channels:
for v := range ch {
fmt.Println(v) // Receive values from ch until it's closed
}
Checking if a Channel is Closed:
v, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
}
Select Statement:
- Purpose: Allows a goroutine to wait on multiple communication operations (sends or receives on multiple channels).
- Similar to
switch
: It has multiplecase
statements, each associated with a communication operation. - Blocking: It blocks until at least one of the communication operations can proceed.
default
: An optionaldefault
case is executed if none of the other cases are ready.
select {
case v := <-ch1:
fmt.Println("Received from ch1:", v)
case v := <-ch2:
fmt.Println("Received from ch2:", v)
case ch3 <- 10:
fmt.Println("Sent 10 to ch3")
default:
fmt.Println("No communication ready")
}
Example: Using select
for Timeout:
select {
case result := <-ch:
fmt.Println("Received result:", result)
case <-time.After(3 * time.Second): // Timeout after 3 seconds
fmt.Println("Timeout!")
}
sync
Package
sync.WaitGroup
: To wait for a collection of goroutines to finish.
Add(delta int)
: Addsdelta
to the counter.Done()
: Decrements the counter by 1 (typically deferred in a goroutine).Wait()
: Blocks until the counter is zero.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the counter when the goroutine finishes
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment the counter for each goroutine
go worker(i, &wg)
}
wg.Wait() // Wait for all workers to finish
fmt.Println("All workers finished.")
}
sync.Mutex
(Mutual Exclusion Lock):
To protect shared resources from concurrent access, ensuring that only one goroutine can access the resource at a time.
Lock()
: Acquires the lock. If the lock is already held, the goroutine blocks until it becomes available.Unlock()
: Releases the lock.
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock() // Acquire the lock
defer mu.Unlock() // Release the lock when the function exits
counter++
fmt.Println("Counter:", counter)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
}
sync.RWMutex
(Read/Write Mutex):
Allows multiple readers or a single writer to access a shared resource concurrently.
RLock()
: Acquires a read lock. Multiple readers can hold the read lock simultaneously.RUnlock()
: Releases a read lock.Lock()
: Acquires a write lock. Only one writer can hold the lock at a time, and no readers are allowed while the write lock is held.Unlock()
: Releases a write lock.
var (
data int
rwMu sync.RWMutex
)
func readData() int {
rwMu.RLock()
defer rwMu.RUnlock()
// Read data
return data
}
func writeData(val int) {
rwMu.Lock()
defer rwMu.Unlock()
// Write data
data = val
}
sync.Once
:
To ensure that a function is executed only once, even if called concurrently from multiple goroutines. Do(f func())
: Takes a function f
as an argument and executes it only once.
var once sync.Once
var config *Config
func GetConfig() *Config {
once.Do(func() { // This function will be executed only once
config = loadConfig() // Assume loadConfig() loads configuration data
})
return config
}
sync.Cond
(Condition Variable):
To provide a way for goroutines to wait for a certain condition to become true, and to signal other goroutines when the condition has changed.
Wait()
: Atomically unlocks the associated mutex and suspends the calling goroutine. When the goroutine is awakened, it reacquires the lock before returning.Signal()
: Wakes up one waiting goroutine (if any).Broadcast()
: Wakes up all waiting goroutines.
Typical Use: Used in conjunction with a sync.Mutex
to protect the condition being checked.
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
ready bool
)
func worker() {
mu.Lock()
for !ready { // Check the condition in a loop
cond.Wait() // Wait until ready is true
}
mu.Unlock()
// ... do work ...
}
func main() {
// ... start multiple worker goroutines ...
// ... some time later ...
mu.Lock()
ready = true // Change the condition
mu.Unlock()
cond.Broadcast() // Notify all waiting goroutines
}
Worker Pools
A fixed number of worker goroutines are created to process tasks from a queue (typically a channel).
Benefits:
- Limits resource usage by controlling the number of concurrent workers.
- Improves performance by reusing goroutines instead of creating new ones for each task.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "started job", j)
time.Sleep(time.Second) // Simulate work
fmt.Println("worker", id, "finished job", j)
results <- j * 2 // Send result
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// Start worker goroutines
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send jobs to the workers
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // Close the jobs channel to signal that no more jobs will be sent
// Collect results
for a := 1; a <= numJobs; a++ {
<-results
}
}
Fan-Out, Fan-In
- Fan-Out: A single goroutine distributes tasks to multiple worker goroutines.
- Fan-In: Multiple goroutines send results to a single goroutine that collects and processes them.
package main
import (
"fmt"
"sync"
)
func producer(nums []int) <-chan int { // Fan-out: Produces data
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int { // Worker: Processes data
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func merge(cs ...<-chan int) <-chan int { // Fan-in: Merges results
var wg sync.WaitGroup
out := make(chan int)
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// Start a goroutine to close out once all output goroutines are done
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
in := producer([]int{1, 2, 3, 4, 5})
// Fan-out: Start two square worker goroutines
c1 := square(in)
c2 := square(in)
// Fan-in: Merge results from c1 and c2
for n := range merge(c1, c2) {
fmt.Println(n)
}
}
Pipelines
A series of stages connected by channels, where each stage performs a specific operation on the data flowing through the pipeline.
package main
import "fmt"
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Set up the pipeline: generator -> square -> output
c := generator(2, 3)
out := square(c)
// Consume the output
for n := range out {
fmt.Println(n) // Output: 4 9
}
}
Testing and Benchmarking
The testing
Package: The core of Go's testing capabilities is the testing
package in the standard library. It provides the tools you need to write and run tests.
Test Files:
- Test files have names that end with
_test.go
. For example, if you have a file namedmycode.go
, your test file would bemycode_test.go
. - Test files reside in the same package as the code they are testing.
Test Functions:
- Test functions have names that start with
Test
followed by a capitalized word or phrase (e.g.,TestCalculateSum
,TestValidateInput
). - They take a single argument of type
*testing.T
. - They are used to make assertions about the behavior of your code.
Running Tests:
go test
: Runs tests in the current package.go test ./...
: Runs tests in the current directory and all subdirectories.go test -v
: Runs tests in verbose mode, showing the results of each individual test.go test -run <regex>
: Runs only tests whose names match the regular expression.go test -cover
: Shows test coverage information (what percentage of your code is executed by tests).go test -coverprofile=cover.out
: Generates a coverage profile file.go tool cover -html=cover.out
: Displays the coverage profile in a web browser, highlighting covered and uncovered code.
Unit Testing with testing.T
Assertions: Test functions use methods on the *testing.T
object to report failures:
t.Error(args ...)
: Reports a non-fatal error. The test continues execution.t.Errorf(format string, args ...)
: Reports a non-fatal error with a formatted message.t.Fatal(args ...)
: Reports a fatal error. The test stops execution immediately.t.Fatalf(format string, args ...)
: Reports a fatal error with a formatted message.t.Log(args ...)
: Logs information (useful for debugging).t.Logf(format string, args ...)
: Logs formatted information.t.Helper()
: Marks the calling function as a test helper function. When a test fails, the error will be reported on the line of code where the helper function was called from, making it easier to identify the source of the failure.
// mycode.go
package mypackage
func Add(x, y int) int {
return x + y
}
// mycode_test.go
package mypackage
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Table-Driven Tests: A powerful pattern for testing multiple inputs and expected outputs concisely.
// mycode_test.go
package mypackage
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
x int
y int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero", 0, 5, 5},
{"zero values", 0, 0, 0}, // Example of a descriptive name
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // Run each test case as a subtest
result := Add(tt.x, tt.y)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.x, tt.y, result, tt.expected)
}
})
}
}
Setup and Teardown:
TestMain
: A special function that can be used to perform setup and teardown for an entire test suite (all tests in a package). It's called only once per package.
func TestMain(m *testing.M) {
// Perform setup (e.g., create a database connection, initialize resources)
fmt.Println("Setup for the test suite")
exitCode := m.Run() // Run the tests
// Perform teardown (e.g., close the database connection, clean up resources)
fmt.Println("Teardown for the test suite")
os.Exit(exitCode)
}
t.Cleanup(func())
: Registers a function to be called when the test (or subtest) completes. Useful for cleaning up resources specific to a test.
func TestSomething(t *testing.T) {
// ... setup for the test ...
t.Cleanup(func() {
// Clean up resources for this test (e.g., close a file, delete temporary data)
fmt.Println("Cleaning up after TestSomething")
})
// ... test logic ...
}
Helper Functions: Functions that encapsulate common setup or assertion logic. Use t.Helper()
to mark them as helper functions.
func assertEqual(t *testing.T, got, want int) {
t.Helper() // Mark this function as a helper
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMyFunction(t *testing.T) {
result := MyFunction(5)
assertEqual(t, result, 10) // Errors will be reported on this line
}
Mocking (Beyond the Standard Library)
What is Mocking? Replacing dependencies of your code with controlled substitutes (mocks) for testing purposes. This allows you to isolate the unit under test and simulate different behaviors of its dependencies.
Why Mock?
- Isolation: Test units in isolation, without relying on external systems or the real implementations of dependencies.
- Control: Simulate various responses and error conditions from dependencies.
- Speed: Mocking can make tests faster, especially when dealing with slow dependencies like databases or network call
Popular Mocking Libraries:
testify/mock
: (https://github.com/stretchr/testify) A widely used library providing a flexible way to define mock objects and expectations.golang/mock
(gomock): (https://github.com/golang/mock) Another popular option, often used with itsmockgen
tool to generate mock code based on interfaces.
Example (Illustrative — using testify/mock
):
// myapp.go
package myapp
type Database interface {
GetUserName(userID int) (string, error)
}
type MyApp struct {
db Database
}
func (a *MyApp) GetUserGreeting(userID int) (string, error) {
userName, err := a.db.GetUserName(userID)
if err != nil {
return "", err
}
return "Hello, " + userName + "!", nil
}
// myapp_test.go
import (
"errors"
"testing"
"github.com/stretchr/testify/mock"
)
// MockDatabase is a mock implementation of the Database interface
type MockDatabase struct {
mock.Mock
}
func (m *MockDatabase) GetUserName(userID int) (string, error) {
args := m.Called(userID) // Record that the method was called with userID
return args.String(0), args.Error(1) // Return values set by expectations
}
func TestGetUserGreeting(t *testing.T) {
// Create a mock database
mockDB := new(MockDatabase)
// Set up expectations for the mock
mockDB.On("GetUserName", 123).Return("Alice", nil) // Expect GetUserName(123) to be called, return "Alice", nil
mockDB.On("GetUserName", 456).Return("", errors.New("user not found")) // Expect GetUserName(456) to be called, return "", error
// Create an instance of MyApp with the mock database
app := &MyApp{db: mockDB}
// Test case 1: Successful retrieval
greeting, err := app.GetUserGreeting(123)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if greeting != "Hello, Alice!" {
t.Errorf("got %s; want %s", greeting, "Hello, Alice!")
}
// Test case 2: Error retrieval
_, err = app.GetUserGreeting(456)
if err == nil {
t.Fatal("expected error, got nil")
}
// Assert that all expectations were met
mockDB.AssertExpectations(t)
}
Reference
Happy learning golang :-)