Skip to content

Go Quick Guide

This guide provides a rapid overview of the Go programming language, covering its fundamental aspects from environment setup to core language features. It’s designed for both beginners and those looking for a quick refresher.

Go, also known as Golang, is an open-source programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. It was publicly announced in November 2009. Go is a statically typed, compiled language renowned for its simplicity, efficiency, strong support for concurrent programming, and robust standard library. It’s well-suited for systems programming, web development, network services, and command-line tools.

Go programs are constructed using packages, which aid in efficient dependency management. The Go toolchain uses a traditional compile-and-link model to generate statically linked executable binaries, simplifying deployment as they typically have no external dependencies.

  • Simplicity and Readability: Go aims for a clean and concise syntax, making code easier to read and maintain.
  • Concurrency: Built-in support for concurrent programming via goroutines (lightweight threads) and channels (for communication between goroutines), along with the select statement.
  • Fast Compilation: Compilation times are notably fast, improving developer productivity.
  • Static Typing with Type Inference: Strong static typing helps catch errors at compile time, while type inference (e.g., x := 0) reduces verbosity.
  • Garbage Collection: Automatic memory management through garbage collection.
  • Powerful Standard Library: A comprehensive standard library provides a wide range of functionalities.
  • Interfaces for Composition: Implicit interfaces encourage composition over inheritance.
  • Static Linking: Produces single, statically linked binaries by default, simplifying deployment.

Key Language Features (and what’s intentionally different)

Section titled “Key Language Features (and what’s intentionally different)”

To maintain simplicity and achieve its design goals, Go intentionally omits or modifies certain features common in other languages:

  • No Type Inheritance: Go favors composition using struct embedding and interfaces over classical inheritance.
  • No Method or Operator Overloading: Each method and operator has a fixed meaning.
  • No Circular Dependencies: Package dependencies must form a directed acyclic graph.
  • Restricted Pointer Arithmetic: Direct pointer arithmetic is disallowed for safety, though the unsafe package provides low-level capabilities when absolutely necessary.
  • No Assertions (in the traditional sense): Go encourages explicit error handling.
  • Generics: Initially not supported to maintain simplicity, generics were introduced in Go 1.18 to allow writing code that is independent of specific types, enhancing code reusability in certain contexts.
  • No Exceptions (in the try/catch sense): Go uses explicit error return values and the panic/recover mechanism for truly exceptional situations.

A Go program consists of one or more .go source files. These files are organized into packages. An executable program must have a main package and a main function, which serves as the entry point.

This tutorial assumes you are familiar with basic programming concepts and how to edit text files.

Setting up a Go development environment is straightforward. You’ll primarily need the Go distribution, which includes the compiler and standard libraries, and a text editor or IDE.

For quickly trying out Go code without local installation, you can use the Go Playground. Many examples in this tutorial can be run directly there.

  1. Download Go: Visit the official Go downloads page (go.dev/dl/) and download the appropriate installer or archive for your operating system (Windows, macOS, Linux). Follow the installation instructions provided on the site. This typically involves setting up the Go binary in your system’s PATH.
  2. Verify Installation: Open a terminal or command prompt and type go version. You should see the installed Go version.
  3. Text Editor/IDE: Choose a text editor or Integrated Development Environment (IDE). Popular choices include:
    • Visual Studio Code (VS Code) with the Go extension.
    • GoLand (a commercial IDE by JetBrains).
    • Vim, Emacs, Sublime Text with appropriate Go plugins.
  4. Go Workspace and Modules: Traditionally, Go used a GOPATH environment variable to define a workspace. However, since Go 1.11, Go Modules have become the standard for managing dependencies and project structure. Modules allow you to organize your project outside the GOPATH. To start a new project with modules:
    Terminal window
    mkdir myproject
    cd myproject
    go mod init myprojectname # e.g., go mod init github.com/yourusername/myproject
    This creates a go.mod file, which tracks your project’s dependencies.

Writing Your First Go Program (Hello, World!)

Section titled “Writing Your First Go Program (Hello, World!)”
  1. Create a file named hello.go in your project directory (e.g., myproject/hello.go).
  2. Add the following code:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
  1. Run the program from your terminal, inside the myproject directory:
go run hello.go

You should see the output:

Hello, World!

Other useful Go commands include:

  • go build: Compiles your packages and dependencies.
  • go install: Compiles and installs packages or commands.
  • go test: Runs tests.

Understanding the basic structure of a Go program is essential before diving into more complex topics. Let’s break down a minimal Go program.

A Go program generally consists of the following parts:

  • Package Declaration: Every Go program starts with a package declaration. Packages are Go’s way of organizing and reusing code.
  • Import Packages: The import statement is used to include code from other packages.
  • Functions: Code execution is organized into functions. The main function is the entry point for an executable program.
  • Variables: Used to store data.
  • Statements & Expressions: Perform actions and computations.
  • Comments: Ignored by the compiler, used for human-readable explanations.

Consider the “Hello, World!” program:

package main // 1. Package Declaration
import "fmt" // 2. Import Package
// 3. Main function - program entry point
func main() {
// This is a single-line comment
/*
This is a
multi-line comment.
*/
fmt.Println("Hello, World!") // 4. Statement & Expression (calling a function)
}

Let’s examine each part:

  1. package main: Declares that this file belongs to the main package. The main package is special; it defines a standalone executable program, not a library. The main function within this package is where execution begins.
  2. import "fmt": Imports the fmt package (short for format). This package provides functions for formatted I/O, similar to C’s printf and scanf. You can import multiple packages using import ("pkg1"; "pkg2") or import ( "pkg1" "pkg2" ).
  3. func main() { ... }: Defines the main function. This is where the program execution starts.
  4. // ... and /* ... */: These are comments. Single-line comments start with //, and multi-line comments are enclosed in /* and */.
  5. fmt.Println("Hello, World!"): This line calls the Println function from the fmt package. Println prints its arguments to the standard output, followed by a newline character. In Go, names that start with a capital letter (like Println from the fmt package) are exported, meaning they are visible and accessible from other packages that import this package. Names starting with a lowercase letter are private to the package.

To run a Go program:

  1. Save your code in a file with a .go extension (e.g., myprogram.go).
  2. Open a terminal or command prompt.
  3. Navigate to the directory where you saved the file.
  4. Use the command go run myprogram.go.
Example execution:
$ go run hello.go
Hello, World!

Alternatively, you can build an executable binary using go build hello.go, and then run the compiled program (e.g., ./hello on Linux/macOS or hello.exe on Windows).

Now that you’ve seen a basic Go program, let’s delve into some fundamental syntax elements.

A Go program is composed of tokens. A token can be a keyword, an identifier, a constant, a string literal, or a symbol. For example, the statement:

fmt.Println("Hello, World!")

consists of the following tokens:

1. `fmt` (identifier)
2. `.` (symbol - selector)
3. `Println` (identifier)
4. `(` (symbol - opening parenthesis)
5. `"Hello, World!"` (string literal)
6. `)` (symbol - closing parenthesis)

In Go, semicolons (;) are used to terminate statements, but they are mostly optional in your source code. The Go compiler automatically inserts semicolons at the end of most lines according to a few simple rules. This means you usually don’t need to type them yourself, leading to cleaner code.

One place you might see a semicolon is when writing multiple short statements on a single line, for example: a := 1; b := 2.

Comments are ignored by the compiler and are used to add explanations to your code.

  • Single-line comments start with //: // This is a comment
  • Multi-line comments are enclosed in /* and */:
/*
This is a
multi-line comment.
*/

Comments cannot be nested.

An identifier is a name used to identify variables, functions, types, constants, etc. Identifiers must start with a letter (A-Z, a-z) or an underscore (_), followed by zero or more letters, digits (0-9), or underscores.

Examples of valid identifiers: myVar, CalculateValue, _temp, user_id, PI.

Go is case-sensitive, so myVar and MyVar are different identifiers.

Exported vs. Unexported Identifiers: If an identifier starts with a capital letter, it is exported, meaning it’s visible and accessible from other packages. If it starts with a lowercase letter, it is unexported (private) and only accessible within its own package.

The following are reserved keywords in Go and cannot be used as identifiers:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

Whitespace (spaces, tabs, newlines) is used to separate tokens in Go. Blank lines are ignored by the compiler. While Go is flexible with whitespace, idiomatic Go code is typically formatted using the gofmt tool, which enforces a standard style.

For example, in var age int, whitespace is necessary between var, age, and int. However, in x = y + z, whitespace around = and + is for readability but not strictly required by the compiler (though highly recommended and enforced by gofmt).

Data types in Go specify the type of data a variable can hold, how much memory it occupies, and how the bit pattern stored is interpreted. Go is a statically-typed language, meaning variable types are checked at compile time.

Go’s types can be broadly classified into:

CategoryTypes and Description
Basic TypesThese are the fundamental building blocks:
- Boolean: bool (predefined constants true and false).
- Numeric: Integers, floating-point numbers, and complex numbers.
- String: string (a sequence of bytes, typically representing UTF-8 encoded text. Strings are immutable).
Composite TypesThese types are constructed from basic types or other composite types:
- Array: Fixed-size sequence of elements of the same type.
- Slice: Dynamically-sized, flexible view into an underlying array.
- Struct: Collection of fields (variables) of potentially different types, grouped together.
- Pointer: Holds the memory address of a variable.
- Function: A callable unit of code.
- Interface: Defines a set of method signatures. A type implements an interface by implementing its methods.
- Map: Collection of unordered key-value pairs.
- Channel: Used for communication between goroutines (concurrent execution units).

The bool type represents a boolean value, which can be either true or false.

Integer Types: Go offers both signed and unsigned integers of various sizes.

Floating-Point Types:

Complex Types:

The string type represents a sequence of bytes. Strings in Go are immutable, meaning once a string is created, its contents cannot be changed. String literals can be created using double quotes "..." (interpreted, allows escapes like \n) or backticks `...` (raw string literals, no escapes, can span multiple lines).

Derived/Composite Types in Brief (details in later sections)

Section titled “Derived/Composite Types in Brief (details in later sections)”
  • uintptr: An unsigned integer type large enough to store the uninterpreted bits of a pointer value. Used primarily with the unsafe package.

Understanding the appropriate data type for your variables is crucial for writing efficient and correct Go programs. The choice of type affects memory usage, performance, and the operations that can be performed.

A variable is a named storage location that our programs can manipulate. Each variable in Go has a specific type, which determines the size and layout of the variable’s memory, the range of values that can be stored, and the set of operations that can be applied.

Variables must be declared before they are used. Go provides several ways to declare variables.

Using the var keyword: The var keyword declares one or more variables. The type is specified after the variable name(s).

// Declare a single variable
var age int
// Declare multiple variables of the same type
var x, y float64
// Declare variables of different types (using a block)
var (
name string
isEnabled bool
)

Initializing Variables During Declaration: Variables can be initialized at the time of declaration.

var message string = "Hello, Go!"
var count int = 10
var pi float64 = 3.14159
// If an initializer is present, the type can often be omitted (type inference)
var inferredMessage = "Hello again!" // type string is inferred
var inferredCount = 20 // type int is inferred

Short Variable Declaration (:=) Inside a function, the := short assignment statement can be used for declaring and initializing local variables. The type is always inferred from the value on the right-hand side. This form cannot be used for package-level variables.

func main() {
// Short variable declaration
city := "New York" // type string inferred
population := 8400000 // type int inferred
isValid := true // type bool inferred
// Multiple short variable declarations
width, height := 100, 50.5 // width is int, height is float64
fmt.Println(city, population, isValid, width, height)
}

Variables declared without an explicit initial value are given their zero value. The zero value is:

  • 0 for numeric types (integers, floats, complex numbers)
  • false for boolean types
  • "" (the empty string) for string types
  • nil for pointers, functions, interfaces, slices, channels, and maps.
package main
import "fmt"
func main() {
var i int
var f float64
var b bool
var s string
var p *int // pointer
fmt.Printf("int: %d\n", i) // Output: int: 0
fmt.Printf("float64: %f\n", f) // Output: float64: 0.000000
fmt.Printf("bool: %t\n", b) // Output: bool: false
fmt.Printf("string: '%s'\n", s) // Output: string: ''
fmt.Printf("pointer: %v\n", p) // Output: pointer: <nil>
}

The scope of a variable defines where in the program the variable is accessible.

  • Package-level variables (also called global variables) are declared outside any function and are accessible throughout the package, and potentially from other packages if their names are exported (start with a capital letter).
  • Local variables are declared inside a function or a block (e.g., inside an if statement or for loop) and are only accessible within that function or block.

While Go’s specification doesn’t heavily use these C-derived terms, the concepts are relevant:

  • lvalue (locator value): Represents a memory location. Variables are lvalues; they can appear on the left-hand side of an assignment.
  • rvalue (read value): Represents a data value. Literals (like 10 or "hello") are rvalues; they cannot be assigned to and can only appear on the right-hand side of an assignment.

x = 20 is valid (x is an lvalue). 10 = y is invalid (10 is an rvalue).

Constants are fixed values that a program cannot alter during its execution. They are also known as literals when written directly in code. In Go, constants are declared using the const keyword.

Constants can be character, string, boolean, or numeric values.

const Pi float64 = 3.14159265359
const Greeting string = "Hello, Go!"
const IsEnabled bool = true
// Constants can be grouped
const (
StatusOK = 200
StatusError = 500
)
// Type can be omitted for untyped constants (more flexible)
const Version = "1.0.2" // Untyped string constant
const Year = 2023 // Untyped integer constant

Constants in Go can be typed or untyped.

  • Typed constants behave like variables of their declared type: const MyInt int = 10.
  • Untyped constants have a kind (e.g., boolean, rune, integer, floating-point, complex, string) but no fixed type until they are used in a context that requires a specific type. Untyped constants offer more flexibility because they can be used with variables of different compatible types without explicit conversion. Numeric constants are particularly powerful as they support arbitrary precision until assigned or used in a typed context.
const UntypedInt = 100 // Kind: integer
const UntypedFloat = 3.14 // Kind: floating-point
var myFloat32 float32 = UntypedFloat // OK, UntypedFloat converts to float32
var myInt int = UntypedInt // OK, UntypedInt converts to int
// var myByte byte = UntypedInt // This would be OK if 100 fits in a byte
// var myComplex complex64 = UntypedFloat // OK
const TypedInt int = 100
// var myFloat64 float64 = TypedInt // Error: cannot use TypedInt (type int) as type float64 in variable declaration

Integer literals can be decimal, octal (prefix 0o or 0O), hexadecimal (prefix 0x or 0X), or binary (prefix 0b or 0B). Floating-point literals have an integer part, a decimal point, a fractional part, and an optional exponent part (e or E).

const DecimalVal = 99
const OctalVal = 0o17 // 15 in decimal
const HexVal = 0xFF // 255 in decimal
const BinaryVal = 0b1010 // 10 in decimal
const FloatVal = 3.14159
const ExpVal = 6.022e23

String literals are sequences of characters enclosed in double quotes ("...") or backticks (`...`).

  • Double-quoted strings are interpreted: escape sequences like \n (newline), \t (tab), \" (double quote) are processed.
  • Backtick-quoted strings are raw: no escape sequences are processed, and they can span multiple lines.

Rune literals represent a single character (Unicode code point) and are enclosed in single quotes (e.g., 'a', '\n', '\u03A9'). A rune literal has type rune (alias for int32).

const SingleLine = "Hello\nWorld"
const MultiLine = `This is a
raw string literal.
It preserves formatting.`
const MyRune = 'G'
const NewlineRune = '\n'

iota is a special identifier used in constant declarations to create sequences of related values. It simplifies the definition of enumerated constants. iota starts at 0 for each const block and increments by 1 for each subsequent constant specification.

package main
import "fmt"
const (
Red = iota // Red = 0
Green // Green = 1 (iota increments)
Blue // Blue = 2 (iota increments)
)
const (
Read = 1 << iota // Read = 1 (1 << 0)
Write // Write = 2 (1 << 1)
Execute // Execute = 4 (1 << 2)
)
func main() {
fmt.Println(Red, Green, Blue) // Output: 0 1 2
fmt.Println(Read, Write, Execute) // Output: 1 2 4
}

An operator is a symbol that tells the compiler to perform specific mathematical, relational, logical, or bitwise manipulations. Go supports a rich set of operators.

  • Arithmetic Operators
  • Relational Operators
  • Logical Operators
  • Bitwise Operators
  • Assignment Operators
  • Miscellaneous Operators (e.g., address, dereference, channel operators)

Assume A = 10 and B = 20:

OperatorDescriptionExample
+AdditionA + B will give 30
-SubtractionB - A will give 10
*MultiplicationA * B will give 200
/DivisionB / A will give 2. For integers, division truncates towards zero.
%Modulus (remainder)B % A (20 % 10) will give 0. 21 % 10 will give 1.

Increment ++ and Decrement -- Operators: In Go, ++ and -- are statements, not expressions. They can only be used in postfix form (e.g., A++) and do not return a value. They directly modify the variable.

var a = 10
a++ // a becomes 11
fmt.Println(a) // Prints 11
// x = a++ // This is a compile-time error in Go

Assume A = 10 and B = 20:

OperatorDescriptionExample (Result)
==Equal toA == B is false
!=Not equal toA != B is true
>Greater thanA > B is false
<Less thanA < B is true
>=Greater than or equal toA >= B is false
<=Less than or equal toA <= B is true

Assume X = true and Y = false:

OperatorDescriptionExample (Result)
&&Logical ANDX && Y is false. If the left operand is false, the right is not evaluated (short-circuiting).
||Logical ORX || Y is true. If the left operand is true, the right is not evaluated (short-circuiting).
!Logical NOT!X is false.

Bitwise operators work on integers at the binary (bit) level. Assume A = 60 (00111100) and B = 13 (00001101):

OperatorDescriptionExample (Binary -> Decimal)
&Bitwise ANDA & B (00001100) -> 12
|Bitwise ORA | B (00111101) -> 61
^Bitwise XORA ^ B (00110001) -> 49
&^Bit clear (AND NOT)A &^ B (00110000) -> 48. Clears bits in A that are set in B.
<<Left shiftA << 2 (11110000) -> 240. Shifts bits of A left by 2 positions.
>>Right shiftA >> 2 (00001111) -> 15. Shifts bits of A right by 2 positions. For signed negative numbers, it’s an arithmetic shift.

The unary ^ operator is also bitwise NOT (complement): ^A inverts all bits of A.

OperatorDescriptionExample
=Simple assignmentC = A + B assigns value of A + B to C
+=Add and assignC += A is equivalent to C = C + A
-=Subtract and assignC -= A is equivalent to C = C - A
*=Multiply and assignC *= A is equivalent to C = C * A
/=Divide and assignC /= A is equivalent to C = C / A
%=Modulus and assignC %= A is equivalent to C = C % A
<<=Left shift and assignC <<= 2 is equivalent to C = C << 2
>>=Right shift and assignC >>= 2 is equivalent to C = C >> 2
&=Bitwise AND and assignC &= 2 is equivalent to C = C & 2
|=Bitwise OR and assignC |= 2 is equivalent to C = C | 2
^=Bitwise XOR and assignC ^= 2 is equivalent to C = C ^ 2
&^=Bit clear and assignC &^= 2 is equivalent to C = C &^ 2
OperatorDescriptionExample
&Address operator&a returns the memory address of variable a (a pointer).
*Dereference operator (Pointer)*p accesses the value stored at the memory address p (where p is a pointer).
<-Channel operatorUsed for sending and receiving values on channels (e.g., ch <- value to send, value := <-ch to receive).

Operator precedence determines the order in which operations are performed in an expression. Go has a simpler precedence hierarchy compared to languages like C++ or Java. Here’s a summary from highest to lowest precedence:

  1. Unary Operators: + - ! ^ & * <- (highest precedence)
  2. Multiplicative Operators: * / % << >> & &^
  3. Additive Operators: + - | ^
  4. Comparison Operators: == != < <= > >=
  5. Logical AND: &&
  6. Logical OR: || (lowest precedence)

For example, in x = 7 + 3 * 2, multiplication (*) has higher precedence than addition (+), so 3 * 2 is evaluated first, resulting in x being assigned 13 (7 + 6). Parentheses () can be used to override the default precedence: x = (7 + 3) * 2 would result in x being assigned 20.

package main
import "fmt"
func main() {
a := 21
b := 10
var c int
c = a + b
fmt.Printf("Line 1 - Value of c (a+b) is %d\n", c) // 31
c = a - b
fmt.Printf("Line 2 - Value of c (a-b) is %d\n", c) // 11
c = a * b
fmt.Printf("Line 3 - Value of c (a*b) is %d\n", c) // 210
c = a / b
fmt.Printf("Line 4 - Value of c (a/b) is %d\n", c) // 2 (integer division)
c = a % b
fmt.Printf("Line 5 - Value of c (a%%b) is %d\n", c) // 1
a++ // a is now 22
fmt.Printf("Line 6 - Value of a after a++ is %d\n", a)
b-- // b is now 9
fmt.Printf("Line 7 - Value of b after b-- is %d\n", b)
// Relational
if a > b {
fmt.Println("a is greater than b")
}
// Logical
isValid := true
isEnabled := false
if isValid && !isEnabled {
fmt.Println("isValid is true AND isEnabled is false")
}
}

Decision-making structures allow a program to execute different blocks of code based on whether certain conditions are true or false. Go provides several statements for this purpose.

Go’s decision-making statements include:

StatementDescription
if statementExecutes a block of code if a condition is true.
if...else statementExecutes one block of code if a condition is true, and another block if it’s false.
if...else if...else statementTests multiple conditions in sequence.
switch statementCompares an expression against a list of possible values (cases) or evaluates types.
select statementSpecifically used with channels for concurrent operations, waits on multiple channel operations.

Both if and switch statements in Go can include an optional short initialization statement before the condition. Variables declared in this statement are scoped to the blocks of the if or switch statement.

Syntax:

if optional_initialization_statement; condition {
// block of code executed if condition is true
}

Example:

package main
import "fmt"
func main() {
a := 10
if a < 20 {
fmt.Printf("a (%d) is less than 20\n", a)
}
fmt.Println("Value of a is:", a)
// With initialization statement
if b := a * 2; b > 30 {
fmt.Printf("b (%d) is greater than 30\n", b)
} else {
fmt.Printf("b (%d) is not greater than 30\n", b)
}
// fmt.Println(b) // Error: b is not defined here (scoped to if/else)
}

Syntax:

if optional_initialization_statement; condition {
// block for true condition
} else {
// block for false condition
}

Syntax:

if condition1 {
// block for condition1 true
} else if condition2 {
// block for condition2 true
} else if condition3 {
// block for condition3 true
} else {
// block if all preceding conditions are false
}

Example:

package main
import "fmt"
func main() {
score := 85
var grade string
if score >= 90 {
grade = "A"
} else if score >= 80 {
grade = "B"
} else if score >= 70 {
grade = "C"
} else {
grade = "D"
}
fmt.Printf("Score: %d, Grade: %s\n", score, grade) // Score: 85, Grade: B
}

Go’s switch statement is more flexible than in many C-like languages. Cases do not automatically fall through; a fallthrough statement is required for that behavior. switch can operate on expressions (expression switch) or types (type switch).

Expression Switch Syntax:

switch optional_initialization_statement; optional_expression {
case expression_case1, expression_case1_alternative:
// code block
case expression_case2:
// code block
fallthrough // optional: execute next case block too
default:
// code block (optional)
}

If optional_expression is omitted, it defaults to true, allowing case statements to be boolean expressions (like an if-else if-else chain).

Example (Expression Switch):

package main
import (
"fmt"
"time"
)
func main() {
today := time.Now().Weekday()
switch today {
case time.Saturday, time.Sunday:
fmt.Println("It's the weekend!")
default:
fmt.Println("It's a weekday.")
}
// Switch as if-else chain
num := 75
switch {
case num < 0:
fmt.Println("Number is negative")
case num > 0 && num < 100:
fmt.Println("Number is positive and less than 100")
default:
fmt.Println("Number is 0 or 100 or more")
}
}

Type Switch Syntax: A type switch is used to determine the dynamic type of an interface value.

switch v := interface_variable.(type) {
case Type1:
// v is of Type1
case Type2, Type3:
// v is of Type2 or Type3
default:
// v is of some other type
}

Example (Type Switch):

package main
import "fmt"
func printType(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
default:
fmt.Printf("Unknown type: %T, value: %v\n", v, v)
}
}
func main() {
printType(42)
printType("hello")
printType(true)
}

The select statement is used to wait on multiple channel operations. It blocks until one of its cases can run, then it executes that case. If multiple cases are ready, it chooses one at random.

Syntax:

select {
case operation1: // e.g., value := <-channel1 or channel2 <- value
// code block for operation1
case operation2:
// code block for operation2
default:
// code block if no other communication is ready (non-blocking select)
}

Example (select):

package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// Wait for messages from c1 or c2 for up to 3 seconds in total
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("Received from c1:", msg1)
case msg2 := <-c2:
fmt.Println("Received from c2:", msg2)
case <-time.After(3 * time.Second): // Timeout case
fmt.Println("Timeout waiting for messages")
return
}
}
}

This example demonstrates a select statement waiting for messages on two channels, c1 and c2, with a timeout. The output order of “Received from c1” and “Received from c2” might vary depending on which goroutine sends its message first.

Loops are used to execute a block of code repeatedly. Go has only one looping construct: the for loop. However, the for loop is versatile and can be used in ways similar to while and infinite loops in other languages, as well as for iterating over collections.

Go’s for loop has several forms:

  1. The complete for loop (C-style): for initialization; condition; post_statement { ... }
    • initialization: Executed once before the loop starts.
    • condition: Evaluated before each iteration. Loop continues as long as true.
    • post_statement: Executed after each iteration.
for i := 0; i < 5; i++ {
fmt.Println(i) // Prints 0, 1, 2, 3, 4
}
  1. The while loop equivalent: for condition { ... } The initialization and post statements are omitted.
n := 0
for n < 5 {
fmt.Println(n)
n++
} // Prints 0, 1, 2, 3, 4
  1. The infinite loop: for { ... } The condition is omitted, creating an infinite loop. Must be exited using break or return.
sum := 0
for {
sum++
if sum > 10 {
break // Exit the loop
}
} // sum becomes 11
  1. The for...range loop: Used to iterate over elements in data structures like slices, arrays, strings, maps, and channels. for key, value := range collection { ... } for index := range collection { ... } (if only key/index is needed) for _, value := range collection { ... } (if only value is needed)
// Iterating over a slice
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// Iterating over a map
colors := map[string]string{"red": "#ff0000", "green": "#00ff00"}
for key, value := range colors {
fmt.Printf("Key: %s, Value: %s\n", key, value)
}

Go provides statements to control the flow within loops:

Control StatementDescription
breakTerminates the innermost for, switch, or select statement immediately.
continueSkips the remainder of the current iteration of the innermost for loop and proceeds to the next iteration (evaluating the post statement if present).
gotoTransfers control to a labeled statement. Its use is generally discouraged as it can make code harder to read and maintain.

Example with break and continue:

package main
import "fmt"
func main() {
fmt.Println("Using continue:")
for i := 0; i < 5; i++ {
if i == 2 { // Skip iteration when i is 2
continue
}
fmt.Println(i) // Prints 0, 1, 3, 4
}
fmt.Println("\nUsing break:")
sum := 0
for i := 1; i <= 10; i++ {
if sum > 20 {
break // Exit loop if sum exceeds 20
}
sum += i
}
fmt.Println("Sum:", sum) // Sum will be the smallest sum > 20 (e.g., 1+2+3+4+5+6=21)
}

You can use one or more loops inside another for loop.

package main
import "fmt"
func main() {
// Print a 3x3 grid
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
fmt.Printf("(%d,%d) ", i, j)
}
fmt.Println() // Newline after each row
}
}
Output:
(0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
(2,0) (2,1) (2,2)

A function is a reusable block of code that performs a specific task. Every Go program has at least one function: main(), which is the entry point of execution. Functions help organize code, make it more modular, and improve readability.

The general syntax for defining a function in Go is:

func functionName(parameter1 type1, parameter2 type2, ...) (returnType1, returnType2, ...) {
// Function body: statements to perform the task
return value1, value2, ... // Optional return statement
}

Key parts of a function definition:

  • func keyword: Starts the function declaration.
  • functionName: The name of the function. Exported if it starts with a capital letter.
  • Parameters: A list of input values the function accepts. Each parameter has a name and a type. If consecutive parameters have the same type, the type can be written once at the end: (p1, p2 int, p3 string).
  • Return Types: A list of types for the values the function returns. If the function returns multiple values, they are enclosed in parentheses. If it returns nothing, the return type list is omitted.
  • Function Body: The code block containing the statements that define what the function does.
  • return statement: Used to return values from the function. If a function has defined return types, it must end with a return statement (unless it panics or has an infinite loop).

Example: A simple max function

// max returns the larger of two integers
func max(num1, num2 int) int {
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}

To use a function, you call it by its name, providing the required arguments.

package main
import "fmt"
// max returns the larger of two integers
func max(num1, num2 int) int {
if num1 > num2 {
return num1
}
return num2
}
func main() {
a := 100
b := 200
maxValue := max(a, b) // Call the max function
fmt.Printf("Max value is: %d\n", maxValue) // Output: Max value is: 200
}

Go functions can return multiple values. This is commonly used to return a result and an error value.

package main
import (
"fmt"
"errors"
)
// divide returns the result of a/b and an error if b is zero
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil // nil indicates no error
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("10 / 2 =", result) // Output: 10 / 2 = 5
}
result, err = divide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Output: Error: division by zero
} else {
fmt.Println("10 / 0 =", result)
}
}

Return values can be named in the function signature. If so, they are treated as variables defined at the top of the function. A return statement without arguments (a “bare” return) then returns the current values of the named return variables.

func calculate(a, b int) (sum int, difference int) {
sum = a + b
difference = a - b
return // Bare return, returns sum and difference
}
// In main:
// s, d := calculate(10, 5)
// fmt.Println(s, d) // Output: 15 5

While this feature exists, it’s often clearer to be explicit with return statements unless the function is very short.

A function that can be called with a variable number of arguments is a variadic function. The last parameter of a variadic function uses the ... prefix with its type. Inside the function, this parameter is a slice of that type.

package main
import "fmt"
// sumAll calculates the sum of an arbitrary number of integers
func sumAll(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
fmt.Println(sumAll(1, 2, 3)) // Output: 6
fmt.Println(sumAll(10, 20)) // Output: 30
fmt.Println(sumAll()) // Output: 0
// If you have a slice, you can pass its elements as arguments using ...
nums := []int{5, 10, 15}
fmt.Println(sumAll(nums...)) // Output: 30
}

Call by Value vs. Call by Reference (Simulated)

Section titled “Call by Value vs. Call by Reference (Simulated)”

Go always passes arguments to functions by value. This means the function receives a copy of the argument’s value. Modifying the parameter inside the function does not affect the original argument.

To allow a function to modify an original variable, you pass a pointer to that variable. The pointer itself is copied (passed by value), but the copy points to the same memory location as the original pointer, allowing modification of the data at that address.

package main
import "fmt"
// Fails to modify original 'val' because it operates on a copy
func noChange(val int) {
val = 100
}
// Modifies original 'val' by using a pointer
func doChange(valPtr *int) {
*valPtr = 100 // Dereference the pointer to change the value
}
func main() {
x := 10
noChange(x)
fmt.Println("x after noChange:", x) // Output: x after noChange: 10
y := 20
doChange(&y) // Pass the address of y
fmt.Println("y after doChange:", y) // Output: y after doChange: 100
}

Note: Slices, maps, and channels are reference types. When passed to a function, a copy of the descriptor (which includes a pointer to the underlying data structure) is made. Modifications to the elements of a slice or map, or sending/receiving on a channel, will be visible to the caller.

In Go, functions are first-class citizens. They can be assigned to variables, passed as arguments to other functions, and returned from other functions.

An anonymous function is a function without a name. A closure is an anonymous function that accesses variables defined outside its own body (it “closes over” those variables).

package main
import "fmt"
// makeGreeter returns a function that greets a specific name
func makeGreeter(greeting string) func(string) string {
return func(name string) string {
return greeting + ", " + name + "!" // The inner function is a closure; it uses 'greeting'
}
}
func main() {
// Functions as values
add := func(a, b int) int {
return a + b
}
fmt.Println("Sum:", add(5, 3)) // Output: Sum: 8
// Closures
englishGreet := makeGreeter("Hello")
spanishGreet := makeGreeter("Hola")
fmt.Println(englishGreet("Alice")) // Output: Hello, Alice!
fmt.Println(spanishGreet("Bob")) // Output: Hola, Bob!
}

A method is a function associated with a particular type. It’s defined with a receiver argument, which appears before the method name. The receiver connects the method to the named type.

package main
import (
"fmt"
"math"
)
type Circle struct {
Radius float64
}
// Area is a method with a value receiver of type Circle
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Scale is a method with a pointer receiver of type *Circle
// Pointer receivers are used when the method needs to modify the receiver.
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func main() {
myCircle := Circle{Radius: 5}
fmt.Printf("Initial Area: %.2f\n", myCircle.Area()) // Initial Area: 78.54
myCircle.Scale(2) // Go automatically converts myCircle to &myCircle for pointer receivers
fmt.Printf("Radius after scaling: %.2f\n", myCircle.Radius) // Radius after scaling: 10.00
fmt.Printf("Area after scaling: %.2f\n", myCircle.Area()) // Area after scaling: 314.16
}

Scope defines the region of a program where an identifier (like a variable or function name) is accessible. Go uses lexical (or static) scoping, meaning the scope of an identifier is determined by where it is declared in the source code.

  • Universe Block: Encompasses all Go source text. Identifiers declared here are predeclared (e.g., true, false, nil, built-in types like int, string, built-in functions like make, len).
  • Package Block (Package Scope): Identifiers declared at the top level (outside any function) in a package are visible throughout that package. If their names start with a capital letter, they are exported and visible from other packages that import this one.
  • File Block: Each file effectively has its own scope for imports (import "fmt" in one file doesn’t make fmt available in another file in the same package without its own import).
  • Function Scope: Parameters and variables declared inside a function are local to that function and not visible outside it.
  • Block Scope: Variables declared inside a control structure block (e.g., if, for, switch) are scoped to that block (including any else or else if blocks associated with an if).

Variables declared inside a function or block are local variables. They exist only within that function or block.

package main
import "fmt"
func main() {
// a, b, c are local to main()
var a, b int = 10, 20
c := a + b
fmt.Printf("Inside main: a=%d, b=%d, c=%d\n", a, b, c)
if true {
// d is local to this if block
d := 30
fmt.Printf("Inside if block: d=%d\n", d)
}
// fmt.Println(d) // Error: d is not defined here
}

Variables declared outside any function are package-level variables. They are accessible from any function within the same package. If exported, they can be accessed from other packages.

package main
import "fmt"
var packageVar int = 100 // package-level variable
func printPackageVar() {
fmt.Println("From printPackageVar: packageVar =", packageVar)
}
func main() {
fmt.Println("From main: packageVar =", packageVar) // Accessing package-level var
printPackageVar()
packageVar = 200 // Modifying package-level var
fmt.Println("From main after modification: packageVar =", packageVar)
}

If a variable declared in an inner scope has the same name as a variable in an outer scope, the inner variable shadows the outer one. Within the inner scope, the name refers to the inner variable.

package main
import "fmt"
var shadowedVar int = 10 // Package-level variable
func main() {
fmt.Println("Outer scope (before shadowing): shadowedVar =", shadowedVar) // Prints 10
shadowedVar := 20 // Local variable in main, shadows the package-level one
// Note: If we used `shadowedVar = 20` without `:=`, it would assign to the package-level var.
fmt.Println("Inner scope (after shadowing): shadowedVar =", shadowedVar) // Prints 20
if true {
shadowedVar := 30 // Further shadowing within the if block
fmt.Println("Innermost scope: shadowedVar =", shadowedVar) // Prints 30
}
fmt.Println("Back in main's scope: shadowedVar =", shadowedVar) // Prints 20
printPackageLevelVar()
}
func printPackageLevelVar() {
// This function always sees the package-level 'shadowedVar'
fmt.Println("Function printPackageLevelVar: shadowedVar =", shadowedVar) // Prints 10 (or its modified value if changed at package level)
}

It’s generally good practice to avoid excessive shadowing to keep code clear and prevent unintended behavior.

Function parameters are treated as local variables within that function’s scope. They can shadow package-level variables.

package main
import "fmt"
var a int = 100 // Package-level 'a'
func sum(a, b int) int { // 'a' here is a parameter, shadows package-level 'a'
fmt.Printf("Inside sum(): a=%d, b=%d\n", a, b)
return a + b
}
func main() {
var a int = 10 // Local 'a' in main, shadows package-level 'a'
b := 20
fmt.Printf("Inside main(): a=%d, b=%d\n", a, b)
result := sum(a, b) // Passes main's 'a' (10) and 'b' (20) to sum()
fmt.Printf("Result from sum: %d\n", result)
fmt.Printf("Package-level a (accessed via another func or if not shadowed): %d\n", getGlobalA())
}
func getGlobalA() int {
return a // Accesses package-level 'a'
}

Variables declared without an explicit initial value are automatically initialized to their zero value (e.g., 0 for numbers, false for booleans, "" for strings, nil for pointers/slices/maps etc.). This applies to both package-level and local variables.

An array in Go is a fixed-size, numbered sequence of elements of a single, specific type. The size (length) of an array is part of its type. For example, [5]int and [10]int are different array types.

Arrays are value types in Go. When an array is assigned to another variable or passed to a function, a copy of the entire array is made.

Syntax: var arrayName [size]elementType

// Declare an array of 5 integers
var numbers [5]int
// Declare an array of 3 strings
var names [3]string

Arrays can be initialized using an array literal:

// Initialize with values
var primeNumbers = [5]int{2, 3, 5, 7, 11}
// Let the compiler count the elements using ...
weekdays := [...]string{"Mon", "Tue", "Wed", "Thu", "Fri"} // type [5]string
// Initialize specific elements (others get zero value)
// Indices must be within bounds, order doesn't matter for sparse initialization
customInit := [5]int{0: 10, 4: 50} // [10, 0, 0, 0, 50]

If an array is declared without initialization, its elements are automatically set to the zero value for their type.

Elements are accessed using a zero-based index: arrayName[index].

package main
import "fmt"
func main() {
var scores [3]int // Elements initialized to 0
scores[0] = 90
scores[1] = 85
scores[2] = 78
// scores[3] = 100 // Compile-time error: index out of bounds
fmt.Println("Score 1:", scores[0])
fmt.Println("Score 2:", scores[1])
fmt.Println("Score 3:", scores[2])
fmt.Println("All scores:", scores) // Prints [90 85 78]
// Length of an array using len()
fmt.Println("Number of scores:", len(scores)) // Prints 3
// Iterate using a for loop
fmt.Println("Iterating scores:")
for i := 0; i < len(scores); i++ {
fmt.Printf("Score at index %d: %d\n", i, scores[i])
}
// Iterate using for...range
fmt.Println("Iterating scores with range:")
for index, value := range scores {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
}

Go supports multi-dimensional arrays. A common example is a 2D array (matrix).

package main
import "fmt"
func main() {
// Declare and initialize a 2x3 integer array (2 rows, 3 columns)
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println("Matrix:", matrix)
// Access an element
fmt.Println("Element at [1][1]:", matrix[1][1]) // Output: 5
// Iterate through the matrix
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j])
}
}
}

When an array is passed to a function, a copy of the array is passed. Modifications to the array inside the function do not affect the original array. The size of the array is part of its type, so a function expecting [5]int cannot accept [10]int.

package main
import "fmt"
// modifyArray receives a copy of the array
func modifyArray(arr [3]int) {
arr[0] = 100 // Modifies the copy, not the original
fmt.Println("Inside modifyArray:", arr)
}
// To modify the original array, pass a pointer to it
func modifyArrayByPointer(arrPtr *[3]int) {
// (*arrPtr)[0] = 100 // Explicit dereference
// Go allows a shortcut for pointers to arrays/structs:
arrPtr[0] = 100 // Modifies the original array
fmt.Println("Inside modifyArrayByPointer:", *arrPtr)
}
func main() {
originalArray := [3]int{10, 20, 30}
fmt.Println("Original array before modifyArray:", originalArray)
modifyArray(originalArray)
fmt.Println("Original array after modifyArray:", originalArray) // Unchanged
fmt.Println("\nOriginal array before modifyArrayByPointer:", originalArray)
modifyArrayByPointer(&originalArray) // Pass a pointer
fmt.Println("Original array after modifyArrayByPointer:", originalArray) // Changed
}

Due to the copying behavior and fixed size, slices are often preferred over arrays in Go when you need dynamic sizing or want to avoid copying large amounts of data.

A pointer is a variable that stores the memory address of another variable. Pointers are useful for several reasons, including allowing functions to modify their arguments (call by reference simulation) and for working with data structures that require dynamic memory allocation or linking.

Every variable in memory has an address. The & operator (address-of operator) gives you the memory address of a variable.

package main
import "fmt"
func main() {
x := 10
fmt.Printf("Value of x: %d\n", x)
fmt.Printf("Memory address of x: %p\n", &x) // %p formats as a hexadecimal address
}

A pointer type is denoted by *T, where T is the type of the variable the pointer points to. The zero value for a pointer is nil.

var p *int // p is a pointer to an integer, currently nil
var sPtr *string // sPtr is a pointer to a string, currently nil
  1. Assigning an address to a pointer: Use the & operator.
  2. Dereferencing a pointer: Use the * operator (dereference operator) to access the value stored at the address the pointer holds.
package main
import "fmt"
func main() {
a := 42 // An integer variable
var ptrA *int // A pointer to an integer
ptrA = &a // Store the address of 'a' in 'ptrA'
fmt.Printf("Value of a: %d\n", a)
fmt.Printf("Address of a: %p\n", &a)
fmt.Printf("Value of ptrA (the address it holds): %p\n", ptrA)
fmt.Printf("Address of ptrA itself: %p\n", &ptrA)
// Access the value 'a' points to using the pointer
fmt.Printf("Value at address stored in ptrA (*ptrA): %d\n", *ptrA)
// Modify the value of 'a' through the pointer
*ptrA = 100
fmt.Printf("New value of a (modified via ptrA): %d\n", a)
}

A pointer that does not point to any valid memory location is a nil pointer. Its value is nil. Dereferencing a nil pointer will cause a runtime panic.

package main
import "fmt"
func main() {
var p *int
fmt.Printf("Value of p: %v\n", p) // Output: Value of p: <nil>
if p != nil {
fmt.Println("Value pointed to by p:", *p) // This won't run
} else {
fmt.Println("p is a nil pointer")
}
// The following would cause a panic:
// *p = 10
}

Pointers are commonly used with structs, especially when passing large structs to functions to avoid copying, or when methods need to modify the struct’s fields.

type Vertex struct {
X, Y int
}
v := Vertex{1, 2}
pv := &v // pv is a pointer to Vertex v
// To access fields of a struct through a pointer, you can use (*pv).X
// Go provides a shorthand: pv.X automatically dereferences the pointer.
fmt.Println(pv.X) // Prints 1
pv.X = 100
fmt.Println(v.X) // Prints 100 (v was modified)

You can create an array where each element is a pointer.

package main
import "fmt"
const MAX = 3
func main() {
names := [MAX]string{"Alice", "Bob", "Charlie"}
var ptrArray [MAX]*string
for i := 0; i < MAX; i++ {
ptrArray[i] = &names[i] // Assign address of each string element
}
for i := 0; i < MAX; i++ {
fmt.Printf("Value at ptrArray[%d]: %s (Address: %p)\n", i, *ptrArray[i], ptrArray[i])
}
// Modify an original value through the pointer array
*ptrArray[0] = "Alicia"
fmt.Println("Original names array after modification:", names) // [Alicia Bob Charlie]
}

A pointer can also point to another pointer. This is less common in idiomatic Go than in C/C++, but possible.

package main
import "fmt"
func main() {
var num int = 123
var ptr1 *int // Pointer to int
var ptr2 **int // Pointer to pointer to int
ptr1 = &num
ptr2 = &ptr1
fmt.Printf("Value of num = %d\n", num)
fmt.Printf("Value available at *ptr1 = %d\n", *ptr1)
fmt.Printf("Value available at **ptr2 = %d\n", **ptr2)
}

As shown earlier, passing a pointer to a function allows the function to modify the original variable’s value. This is Go’s way of achieving call-by-reference-like behavior.

package main
import "fmt"
func swap(x, y *int) {
temp := *x
*x = *y
*y = temp
}
func main() {
a := 10
b := 20
fmt.Printf("Before swap: a = %d, b = %d\n", a, b)
swap(&a, &b)
fmt.Printf("After swap: a = %d, b = %d\n", a, b)
}

The built-in new() function allocates memory for a type, initializes it to its zero value, and returns a pointer to it. new(T) returns a *T.

ptr := new(int) // ptr is *int, *ptr is 0
fmt.Println(*ptr) // 0
*ptr = 100
fmt.Println(*ptr) // 100

While new() is available, make() is more commonly used for slices, maps, and channels as it initializes their internal structures, not just zeroing memory.

A structure (or struct) in Go is a user-defined composite type that groups together zero or more values (fields) of potentially different types under a single name. Structs are used to represent records or complex data entities.

You define a struct using the type keyword followed by the struct name and the struct keyword with a block of field definitions.

type StructName struct {
FieldName1 type1
FieldName2 type2
// ...
FieldNameN typeN
}
// Example: A Book struct
type Book struct {
Title string
Author string
Subject string
BookID int
Pages int
IsPublished bool
}

Once a struct type is defined, you can create instances (variables) of that type.

// 1. Declare then assign fields (fields get zero values initially)
var book1 Book
book1.Title = "Go Programming Essentials"
book1.Author = "Tech Author"
book1.BookID = 101
// 2. Using a struct literal (order of fields matters if names are omitted)
book2 := Book{"Learning Go", "Jane Doe", "Programming", 102, 300, true}
// 3. Using a struct literal with field names (recommended for clarity)
book3 := Book{
Title: "Advanced Go Patterns",
Author: "John Smith",
BookID: 103,
Pages: 450,
IsPublished: true, // Trailing comma is required if literal is multi-line
}
// 4. Using the new() function (returns a pointer to a zeroed struct)
bookPtr := new(Book)
bookPtr.Title = "Go for Experts" // Go automatically dereferences pointers to structs for field access
// equivalent to (*bookPtr).Title = ...

You access the fields of a struct using the dot . operator.

package main
import "fmt"
type Book struct {
Title string
Author string
BookID int
}
func main() {
myBook := Book{
Title: "The Go Programming Language",
Author: "Alan A. A. Donovan & Brian W. Kernighan",
BookID: 789,
}
fmt.Println("Book Title:", myBook.Title)
fmt.Println("Book Author:", myBook.Author)
fmt.Println("Book ID:", myBook.BookID)
// Modify a field
myBook.BookID = 800
fmt.Println("Updated Book ID:", myBook.BookID)
}

Structs, like other types, can be passed to functions. By default, structs are passed by value (a copy is made). To modify the original struct within a function, pass a pointer to the struct.

package main
import "fmt"
type Book struct {
Title string
Author string
BookID int
}
// printBook accepts a Book value (a copy)
func printBook(b Book) {
fmt.Printf("Title: %s, Author: %s, ID: %d\n", b.Title, b.Author, b.BookID)
b.BookID = 0 // Modifies the copy, not the original
}
// printBookByPointer accepts a pointer to a Book
func printBookByPointer(b *Book) {
fmt.Printf("Title: %s, Author: %s, ID: %d\n", b.Title, b.Author, b.BookID)
b.BookID = 0 // Modifies the original struct via the pointer
}
func main() {
book1 := Book{"Go Recipes", "Various Authors", 201}
fmt.Println("Before calling printBook:", book1.BookID)
printBook(book1)
fmt.Println("After calling printBook:", book1.BookID) // Remains 201
book2 := Book{"Effective Go", "The Go Team", 202}
fmt.Println("\nBefore calling printBookByPointer:", book2.BookID)
printBookByPointer(&book2)
fmt.Println("After calling printBookByPointer:", book2.BookID) // Changes to 0
}

Anonymous Structs: Structs can be defined inline without a name, often for temporary data structures.

Anonymous Fields (Embedding): A struct can have fields without explicit names, by just specifying the type. This is called embedding. Embedded fields’ names are implicitly their type names. Methods of embedded types are promoted to the embedding struct, which is a key way Go achieves composition.

type Person struct {
Name string
Age int
}
type Employee struct {
Person // Embedded Person struct (anonymous field)
EmployeeID string
Salary float64
}
func main() {
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
EmployeeID: "E123",
Salary: 75000,
}
fmt.Println(emp.Name) // Access embedded field directly: Alice
fmt.Println(emp.Person.Age) // Or access via type name: 30
}

A slice is a powerful, flexible, and convenient way to work with sequences of data in Go. Slices are a dynamically-sized, flexible view into the elements of an underlying array. Unlike arrays, the length of a slice can change. Slices are much more common in idiomatic Go than arrays.

Internally, a slice is a small descriptor struct containing three fields:

  1. A pointer to the underlying array.
  2. The length of the segment of the array that the slice contains.
  3. The capacity of the segment (maximum length it can reach by re-slicing).

Slices can be created in several ways:

// 1. Using a slice literal (similar to array literal, but without size)
numbers := []int{10, 20, 30, 40, 50} // Creates an array and a slice that views it
// 2. Using the make() function
// make([]T, length, capacity) - capacity is optional, defaults to length
slice1 := make([]int, 5) // length 5, capacity 5, elements are zeroed [0,0,0,0,0]
slice2 := make([]int, 3, 10) // length 3, capacity 10, elements are zeroed [0,0,0]
// 3. Slicing an existing array or slice
underlyingArray := [5]int{1, 2, 3, 4, 5}
subSlice := underlyingArray[1:4] // Creates a slice [2, 3, 4]. Views elements at index 1, 2, 3.
// low_bound:high_bound (high_bound is exclusive)
// 4. A nil slice (zero value for a slice)
var nilSlice []int // nilSlice is nil, length 0, capacity 0
// An empty slice can also be created: emptySlice := []int{} or make([]int, 0)
  • len(s): Returns the number of elements currently in the slice s.
  • cap(s): Returns the capacity of the slice s (the maximum number of elements it can hold without reallocation of the underlying array).
package main
import "fmt"
func printSliceInfo(name string, s []int) {
fmt.Printf("%s: len=%d cap=%d slice=%v\n", name, len(s), cap(s), s)
}
func main() {
s1 := make([]int, 3, 5)
printSliceInfo("s1", s1) // s1: len=3 cap=5 slice=[0 0 0]
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s2 := arr[2:5]
printSliceInfo("s2", s2) // s2: len=3 cap=8 slice=[2 3 4]
// cap is from index 2 of arr to end of arr
}
  • A nil slice has a value of nil. It has length 0 and capacity 0. It does not point to an underlying array. var s []int
  • An empty slice is a slice with length 0 and capacity 0 (or more), but it is not nil. It may or may not have an underlying array (often a zero-sized array). s1 := []int{} or s2 := make([]int, 0)

For most operations, nil and empty slices behave similarly (e.g., len is 0, range loop does nothing).

The slicing operator s[low:high] creates a new slice that views a portion of the original slice’s (or array’s) underlying array. The new slice shares the same underlying array.

Modifications made through a sub-slice affect the original underlying array and thus other slices sharing that array.

package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println("Original:", numbers)
s1 := numbers[1:4] // [1, 2, 3]
fmt.Println("s1 (numbers[1:4]):", s1)
s2 := numbers[:3] // [0, 1, 2]
fmt.Println("s2 (numbers[:3]):", s2)
s3 := numbers[4:] // [4, 5, 6, 7, 8]
fmt.Println("s3 (numbers[4:]):", s3)
// Modifying an element in a sub-slice changes the original
s1[0] = 100 // numbers[1] becomes 100
fmt.Println("Original after s1[0]=100:", numbers) // [0, 100, 2, 3, 4, 5, 6, 7, 8]
fmt.Println("s2 after s1[0]=100:", s2) // [0, 100, 2] (s2 shares underlying array)
}

The built-in append function adds elements to the end of a slice. If the capacity of the underlying array is insufficient, append allocates a new, larger array, copies the existing elements, and then appends the new elements. The resulting slice (possibly pointing to a new array) is returned.

Syntax: slice = append(slice, elem1, elem2, ...) Syntax: slice = append(slice, anotherSlice...) (appends all elements of anotherSlice)

package main
import "fmt"
func printSliceInfo(name string, s []int) {
fmt.Printf("%s: len=%d cap=%d slice=%v\n", name, len(s), cap(s), s)
}
func main() {
var s []int // A nil slice
printSliceInfo("s (nil)", s)
s = append(s, 0)
printSliceInfo("s (appended 0)", s)
s = append(s, 1)
printSliceInfo("s (appended 1)", s)
s = append(s, 2, 3, 4) // Append multiple elements
printSliceInfo("s (appended 2,3,4)", s)
s = append(s, []int{5,6,7}...) // Append another slice
printSliceInfo("s (appended slice)", s)
}
Output might look like (capacities can vary depending on Go's growth strategy):
s (nil): len=0 cap=0 slice=[]
s (appended 0): len=1 cap=1 slice=[0]
s (appended 1): len=2 cap=2 slice=[0 1]
s (appended 2,3,4): len=5 cap=6 slice=[0 1 2 3 4]
s (appended slice): len=8 cap=12 slice=[0 1 2 3 4 5 6 7]

The built-in copy function copies elements from a source slice to a destination slice. copy(dst, src) copies min(len(dst), len(src)) elements. It returns the number of elements copied. The source and destination may overlap.

package main
import "fmt"
func main() {
src := []int{1, 2, 3}
dst := make([]int, 2) // Destination slice with length 2
numCopied := copy(dst, src)
fmt.Println("Destination slice (dst):", dst) // Output: [1 2]
fmt.Println("Number of elements copied:", numCopied) // Output: 2
dstFull := make([]int, len(src))
copy(dstFull, src)
fmt.Println("Full copy (dstFull):", dstFull) // Output: [1 2 3]
}

The range keyword is used in a for loop to iterate over elements of various data structures like arrays, slices, strings, maps, and channels.

The range keyword returns one or two values depending on the collection being iterated:

Collection TypeFirst Value (Index/Key)Second Value (Element)
Array or Slice ([n]T or []T)index (int)element (T)
String (string)index (int, byte index of UTF-8 codepoint)rune (int32, Unicode code point)
Map (map[K]V)key (K)value (V)
Channel (chan T)element (T)(No second value. Iterates until channel is closed)

If you only need the first value (e.g., index or key), you can omit the second: for indexOrKey := range collection { ... }

If you only need the second value (e.g., element), use the blank identifier _ to discard the first: for _, element := range collection { ... }

package main
import "fmt"
func main() {
// --- Iterating over a Slice ---
numbers := []int{10, 20, 30, 40}
fmt.Println("Iterating over slice 'numbers':")
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// Summing elements of a slice
sum := 0
for _, num := range numbers { // We only need the value, so discard index
sum += num
}
fmt.Println("Sum of numbers:", sum)
// --- Iterating over a Map ---
// Note: Iteration order for maps is not guaranteed.
countryCapitalMap := map[string]string{
"France": "Paris",
"Italy": "Rome",
"Japan": "Tokyo",
}
fmt.Println("\nIterating over map 'countryCapitalMap':")
for country, capital := range countryCapitalMap {
fmt.Printf("Capital of %s is %s\n", country, capital)
}
// Only keys from a map
fmt.Println("\nCountries in map:")
for country := range countryCapitalMap {
fmt.Println(country)
}
// --- Iterating over a String ---
// Range over a string iterates through Unicode code points (runes).
// The index is the starting byte position of the rune.
str := "Go语言"
fmt.Println("\nIterating over string 'str':")
for index, charRune := range str {
fmt.Printf("Byte Index: %d, Character: %c (Rune: %U)\n", index, charRune, charRune)
}
}
Output (map iteration order might vary):
Iterating over slice 'numbers':
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
Index: 3, Value: 40
Sum of numbers: 100
Iterating over map 'countryCapitalMap':
Capital of France is Paris
Capital of Italy is Rome
Capital of Japan is Tokyo
Countries in map:
France
Italy
Japan
Iterating over string 'str':
Byte Index: 0, Character: G (Rune: U+0047)
Byte Index: 1, Character: o (Rune: U+006F)
Byte Index: 2, Character: 语 (Rune: U+8BED)
Byte Index: 5, Character: 言 (Rune: U+8A00)

Important Note on range and Value Copying:

Section titled “Important Note on range and Value Copying:”

When ranging over an array or slice, range provides a copy of each element. If you need to modify the original elements, you must use the index to access them directly: collection[index] = newValue.

nums := []int{1, 2, 3}
for _, val := range nums { // val is a copy
val *= 10 // This modifies the copy, not the original slice element
}
fmt.Println(nums) // Output: [1 2 3]
for i := range nums { // To modify, use the index
nums[i] *= 10
}
fmt.Println(nums) // Output: [10 20 30]

Recursion is a programming technique where a function calls itself to solve a problem. A recursive function typically has two parts:

  1. Base Case(s): One or more conditions that stop the recursion, preventing an infinite loop.
  2. Recursive Step: The part of the function where it calls itself, usually with a modified argument that moves closer to a base case.

A simple conceptual example:

func recursiveFunction(parameters) {
if base_condition_is_met {
// Solve directly and return (base case)
return some_value
} else {
// Modify parameters towards base case
// Call recursiveFunction with new parameters (recursive step)
recursive_result := recursiveFunction(modified_parameters)
// Optionally, process recursive_result and return
return processed_result
}
}

Go supports recursion. However, programmers must ensure a well-defined base case to avoid infinite recursion, which can lead to a stack overflow error.

The factorial of a non-negative integer n (denoted n!) is the product of all positive integers less than or equal to n. 0! = 1.

package main
import "fmt"
// factorial calculates n!
// Note: For large n, this can overflow int/uint64. Error handling for n < 0 omitted for simplicity.
func factorial(n uint64) uint64 {
// Base case: if n is 0 or 1, factorial is 1
if n <= 1 {
return 1
}
// Recursive step: n * factorial(n-1)
return n * factorial(n-1)
}
func main() {
var num uint64 = 5
fmt.Printf("Factorial of %d is %d\n", num, factorial(num)) // Output: Factorial of 5 is 120
num = 0
fmt.Printf("Factorial of %d is %d\n", num, factorial(num)) // Output: Factorial of 0 is 1
// Be cautious with large numbers, e.g., factorial(20) is large
// factorial(65) would overflow uint64
num = 20
fmt.Printf("Factorial of %d is %d\n", num, factorial(num))
}
Output for `factorial(20)`:
Factorial of 20 is 2432902008176640000

The Fibonacci sequence is a series where each number is the sum of the two preceding ones, usually starting with 0 and 1: 0, 1, 1, 2, 3, 5, 8, ...

package main
import "fmt"
// fibonacci returns the n-th Fibonacci number
// This recursive implementation is simple but highly inefficient due to repeated calculations.
func fibonacci(n int) int {
if n <= 0 { // Base case 1
return 0
}
if n == 1 { // Base case 2
return 1
}
// Recursive step
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
fmt.Println("Fibonacci sequence (first 10 terms):")
for i := 0; i < 10; i++ {
fmt.Printf("%d ", fibonacci(i))
}
fmt.Println()
// Output: 0 1 1 2 3 5 8 13 21 34
}

Pros:

  • Can lead to elegant and simple solutions for problems that are naturally recursive (e.g., tree traversals, divide and conquer algorithms).
  • Code can be easier to read and understand for such problems.

Cons:

  • Can be less efficient than iterative solutions due to function call overhead.
  • Can lead to stack overflow errors for deep recursion levels, as each function call consumes stack space.
  • Some recursive algorithms (like the naive Fibonacci example) can be very inefficient due to recomputing the same values multiple times. Memoization or dynamic programming can mitigate this.

In Go, while recursion is supported, iterative solutions are often preferred for performance and to avoid stack depth limitations, especially if the recursion is not tail-call optimizable (Go currently does not guarantee tail-call optimization).

Go is a statically-typed language and generally requires explicit conversions between different types. This is often referred to as “type casting” in other languages, but in Go, the term type conversion is more accurate. Implicit type conversions are rare in Go (e.g., assigning an untyped constant to a typed variable).

The syntax for converting a value v to a type T is T(v).

var myInt int = 10
var myFloat float64 = float64(myInt) // Convert int to float64
var myFloatVal float64 = 3.14
// var myIntVal int = myFloatVal // Compile error: cannot use myFloatVal (type float64) as type int in assignment
var myIntVal int = int(myFloatVal) // Convert float64 to int (truncates decimal part)

Numeric Conversions: Converting between different numeric types (e.g., int to float64, int32 to int64). Be mindful of potential data loss (e.g., converting a large float64 to int truncates the decimal part; converting a large int64 to int32 might overflow if the value doesn’t fit).

package main
import (
"fmt"
"math"
)
func main() {
var i int = 42
var f float64 = float64(i) // int to float64
var u uint = uint(f) // float64 to uint (decimal part truncated)
fmt.Printf("i: %T, %v\n", i, i)
fmt.Printf("f: %T, %v\n", f, f)
fmt.Printf("u: %T, %v\n", u, u)
sum := 17
count := 5
// var mean float32 = sum / count // Error: integer division, then type mismatch
var mean float32 = float32(sum) / float32(count)
fmt.Printf("Value of mean: %f\n", mean) // Output: Value of mean: 3.400000
// Potential data loss
largeFloat := 12345.6789
smallInt := int(largeFloat) // smallInt will be 12345 (truncated)
fmt.Printf("largeFloat: %f, smallInt: %d\n", largeFloat, smallInt)
bigInt64 := int64(math.MaxInt32 + 10)
smallInt32 := int32(bigInt64) // Potential overflow if bigInt64 > MaxInt32 or < MinInt32
// Behavior on overflow is platform-dependent or wraps around.
fmt.Printf("bigInt64: %d, smallInt32: %d\n", bigInt64, smallInt32)
}

String Conversions:

  • Integer to String: Use strconv.Itoa() (Integer to ASCII) or fmt.Sprintf().
  • String to Integer: Use strconv.Atoi() (ASCII to Integer) or strconv.ParseInt().
  • Byte Slice to String: string(byteSlice).
  • String to Byte Slice: []byte(str).
package main
import (
"fmt"
"strconv"
)
func main() {
num := 123
strNum := strconv.Itoa(num)
fmt.Printf("String from int: %s (type %T)\n", strNum, strNum)
strVal := "456"
numVal, err := strconv.Atoi(strVal)
if err != nil {
fmt.Println("Error converting string to int:", err)
} else {
fmt.Printf("Int from string: %d (type %T)\n", numVal, numVal)
}
byteSlice := []byte{ 'h', 'e', 'l', 'l', 'o' }
strFromBytes := string(byteSlice)
fmt.Printf("String from byte slice: %s\n", strFromBytes)
originalStr := "world"
bytesFromStr := []byte(originalStr)
fmt.Printf("Byte slice from string: %v\n", bytesFromStr)
}

It’s important to distinguish type conversions from type assertions:

  • Type Conversion (T(v)): Creates a new value of type T from value v. It applies between types that are explicitly convertible (e.g., different numeric types, string and byte/rune slices).
  • Type Assertion (v.(T)): Used with interface types. It checks if the dynamic type of the interface value v is T and extracts the underlying value of type T. It doesn’t convert the value; it reveals its actual underlying type.

Go’s emphasis on explicit type conversion helps prevent subtle bugs that can arise from unintended implicit conversions in other languages.

Go has a distinctive approach to error handling. Instead of exceptions (like try-catch blocks in Java or Python), Go functions that can fail typically return an additional value of type error. An error value of nil indicates success, while a non-nil value indicates failure.

error is a built-in interface type in Go, defined as:

type error interface {
Error() string
}

Any type that implements the Error() string method satisfies the error interface. The Error() method should return a human-readable description of the error.

By convention, if a function can produce an error, it returns the error as its last return value. The caller then checks if the returned error is nil.

package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("non_existent_file.txt")
if err != nil {
// Handle the error
fmt.Println("Error opening file:", err)
// Often, you might want to log the error and exit, or return it further up the call stack.
// For this example, we'll just print and continue.
} else {
// Work with the file
fmt.Println("File opened successfully:", file.Name())
file.Close()
}
// Example of a function that returns an error
result, err := processData("")
if err != nil {
fmt.Println("Error processing data:", err)
} else {
fmt.Println("Processed data:", result)
}
}

Go provides several ways to create error values:

  1. errors.New(message string) error: From the errors package. Creates a simple error with the given message.
import "errors"
func doSomething(input string) error {
if input == "" {
return errors.New("input cannot be empty")
}
// ... do something ...
return nil
}
  1. fmt.Errorf(format string, args ...interface{}) error: From the fmt package. Creates an error with a formatted message, similar to fmt.Sprintf.
import "fmt"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide %.2f by zero", a)
}
return a / b, nil
}
  1. Custom Error Types: You can define your own struct type that implements the error interface. This allows you to include more contextual information in your errors.
package main
import "fmt"
// MyError is a custom error type
type MyError struct {
Operation string
Message string
ErrorCode int
}
func (e *MyError) Error() string {
return fmt.Sprintf("Operation '%s' failed: %s (Code: %d)", e.Operation, e.Message, e.ErrorCode)
}
func performOperation(shouldFail bool) error {
if shouldFail {
return &MyError{
Operation: "Data Fetch",
Message: "Network timeout",
ErrorCode: 504,
}
}
return nil
}
func main() {
err := performOperation(true)
if err != nil {
fmt.Println("Custom Error:", err)
// Type assertion to access custom fields
if myErr, ok := err.(*MyError); ok {
fmt.Println("Specific Error Code:", myErr.ErrorCode)
}
}
}

Go 1.13 introduced error wrapping, allowing an error to contain another error, creating a chain of errors. This helps preserve context from lower-level errors.

  • fmt.Errorf with the %w verb wraps an error.
  • errors.Unwrap(err error) error retrieves the wrapped error.
  • errors.Is(err, target error) bool checks if err (or any error in its chain) matches target.
  • errors.As(err error, target interface{}) bool checks if err (or any error in its chain) is of a specific custom type and assigns it to target.
package main
import (
"errors"
"fmt"
"os"
)
var ErrCustom = errors.New("a custom sentinel error")
func readFileAndProcess() error {
data, err := os.ReadFile("somefile.txt")
if err != nil {
// Wrap the original error from os.ReadFile
return fmt.Errorf("failed to read and process file: %w", err)
}
// Process data...
if len(data) == 0 {
return ErrCustom
}
return nil
}
func main() {
err := readFileAndProcess()
if err != nil {
fmt.Println("Original error:", err)
// Check if the error is os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist.")
}
// Check if the error is our custom sentinel error
if errors.Is(err, ErrCustom) {
fmt.Println("Encountered our custom error.")
}
// Unwrap to get the underlying error (if any)
underlyingErr := errors.Unwrap(err)
if underlyingErr != nil {
fmt.Println("Underlying error:", underlyingErr)
}
}
}

Go also has panic and recover mechanisms, but they are used for truly exceptional situations, not for normal error handling.

  • panic(v interface{}): Stops the normal execution of the current goroutine. The panic continues up the call stack, running any deferred functions along the way.
  • recover() interface{}: Regains control of a panicking goroutine. recover is only useful inside deferred functions. If the current goroutine is not panicking, recover returns nil.

Idiomatic Go strongly prefers explicit error returns over panic for most error conditions.