Go Quick Guide
Go - Quick Guide
Section titled “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 - Overview
Section titled “Go - Overview”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.
Design Principles
Section titled “Design Principles”- 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
selectstatement. - 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
unsafepackage 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/recovermechanism for truly exceptional situations.
Go Programs
Section titled “Go Programs”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.
Go - Environment Setup
Section titled “Go - Environment Setup”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.
Online Go Playground
Section titled “Online Go Playground”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.
Local Environment Setup
Section titled “Local Environment Setup”- 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.
- Verify Installation: Open a terminal or command prompt and type
go version. You should see the installed Go version. - 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.
- Go Workspace and Modules: Traditionally, Go used a
GOPATHenvironment 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 theGOPATH. To start a new project with modules:This creates aTerminal window mkdir myprojectcd myprojectgo mod init myprojectname # e.g., go mod init github.com/yourusername/myprojectgo.modfile, which tracks your project’s dependencies.
Writing Your First Go Program (Hello, World!)
Section titled “Writing Your First Go Program (Hello, World!)”- Create a file named
hello.goin your project directory (e.g.,myproject/hello.go). - Add the following code:
package main
import "fmt"
func main() { fmt.Println("Hello, World!")}- Run the program from your terminal, inside the
myprojectdirectory:
go run hello.goYou 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.
Go - Program Structure
Section titled “Go - Program Structure”Understanding the basic structure of a Go program is essential before diving into more complex topics. Let’s break down a minimal Go program.
Go Hello World Example Revisited
Section titled “Go Hello World Example Revisited”A Go program generally consists of the following parts:
- Package Declaration: Every Go program starts with a
packagedeclaration. Packages are Go’s way of organizing and reusing code. - Import Packages: The
importstatement is used to include code from other packages. - Functions: Code execution is organized into functions. The
mainfunction 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 pointfunc 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:
package main: Declares that this file belongs to themainpackage. Themainpackage is special; it defines a standalone executable program, not a library. Themainfunction within this package is where execution begins.import "fmt": Imports thefmtpackage (short for format). This package provides functions for formatted I/O, similar to C’sprintfandscanf. You can import multiple packages usingimport ("pkg1"; "pkg2")orimport ( "pkg1" "pkg2" ).func main() { ... }: Defines themainfunction. This is where the program execution starts.// ...and/* ... */: These are comments. Single-line comments start with//, and multi-line comments are enclosed in/*and*/.fmt.Println("Hello, World!"): This line calls thePrintlnfunction from thefmtpackage.Printlnprints its arguments to the standard output, followed by a newline character. In Go, names that start with a capital letter (likePrintlnfrom thefmtpackage) 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.
Executing a Go Program
Section titled “Executing a Go Program”To run a Go program:
- Save your code in a file with a
.goextension (e.g.,myprogram.go). - Open a terminal or command prompt.
- Navigate to the directory where you saved the file.
- Use the command
go run myprogram.go.
Example execution:$ go run hello.goHello, 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).
Go - Basic Syntax
Section titled “Go - Basic Syntax”Now that you’ve seen a basic Go program, let’s delve into some fundamental syntax elements.
Tokens in Go
Section titled “Tokens in Go”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)Semicolons
Section titled “Semicolons”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
Section titled “Comments”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 amulti-line comment.*/Comments cannot be nested.
Identifiers
Section titled “Identifiers”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.
Keywords
Section titled “Keywords”The following are reserved keywords in Go and cannot be used as identifiers:
| break | default | func | interface | select |
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrough | if | range | type |
| continue | for | import | return | var |
Whitespace in Go
Section titled “Whitespace in Go”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).
Go - Data Types
Section titled “Go - Data Types”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:
| Category | Types and Description |
|---|---|
| Basic Types | These 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 Types | These 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). |
Boolean Types
Section titled “Boolean Types”The bool type represents a boolean value, which can be either true or false.
Numeric Types
Section titled “Numeric Types”Integer Types: Go offers both signed and unsigned integers of various sizes.
Floating-Point Types:
Complex Types:
String Types
Section titled “String 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 theunsafepackage.
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.
Go - Variables
Section titled “Go - Variables”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.
Variable Declaration
Section titled “Variable Declaration”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 variablevar age int
// Declare multiple variables of the same typevar 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 = 10var pi float64 = 3.14159
// If an initializer is present, the type can often be omitted (type inference)var inferredMessage = "Hello again!" // type string is inferredvar inferredCount = 20 // type int is inferredShort 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)}Zero Values
Section titled “Zero Values”Variables declared without an explicit initial value are given their zero value. The zero value is:
0for numeric types (integers, floats, complex numbers)falsefor boolean types""(the empty string) for string typesnilfor 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>}Scope of Variables
Section titled “Scope of Variables”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
ifstatement orforloop) and are only accessible within that function or block.
Lvalues and Rvalues (Conceptual)
Section titled “Lvalues and Rvalues (Conceptual)”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
10or"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).
Go - Constants
Section titled “Go - Constants”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.
Constant Declaration
Section titled “Constant Declaration”Constants can be character, string, boolean, or numeric values.
const Pi float64 = 3.14159265359const Greeting string = "Hello, Go!"const IsEnabled bool = true
// Constants can be groupedconst ( StatusOK = 200 StatusError = 500)
// Type can be omitted for untyped constants (more flexible)const Version = "1.0.2" // Untyped string constantconst Year = 2023 // Untyped integer constantTyped vs. Untyped Constants
Section titled “Typed vs. Untyped Constants”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: integerconst UntypedFloat = 3.14 // Kind: floating-point
var myFloat32 float32 = UntypedFloat // OK, UntypedFloat converts to float32var 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 declarationNumeric Constants
Section titled “Numeric Constants”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 = 99const OctalVal = 0o17 // 15 in decimalconst HexVal = 0xFF // 255 in decimalconst BinaryVal = 0b1010 // 10 in decimal
const FloatVal = 3.14159const ExpVal = 6.022e23String Literals and Rune Literals
Section titled “String Literals and Rune Literals”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 araw string literal.It preserves formatting.`
const MyRune = 'G'const NewlineRune = '\n'The iota Constant Generator
Section titled “The iota Constant Generator”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}Go - Operators
Section titled “Go - Operators”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)
Arithmetic Operators
Section titled “Arithmetic Operators”Assume A = 10 and B = 20:
| Operator | Description | Example |
|---|---|---|
| + | Addition | A + B will give 30 |
| - | Subtraction | B - A will give 10 |
| * | Multiplication | A * B will give 200 |
| / | Division | B / 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 = 10a++ // a becomes 11fmt.Println(a) // Prints 11// x = a++ // This is a compile-time error in GoRelational Operators
Section titled “Relational Operators”Assume A = 10 and B = 20:
| Operator | Description | Example (Result) |
|---|---|---|
| == | Equal to | A == B is false |
| != | Not equal to | A != B is true |
| > | Greater than | A > B is false |
| < | Less than | A < B is true |
| >= | Greater than or equal to | A >= B is false |
| <= | Less than or equal to | A <= B is true |
Logical Operators
Section titled “Logical Operators”Assume X = true and Y = false:
| Operator | Description | Example (Result) |
|---|---|---|
| && | Logical AND | X && Y is false. If the left operand is false, the right is not evaluated (short-circuiting). |
| || | Logical OR | X || Y is true. If the left operand is true, the right is not evaluated (short-circuiting). |
| ! | Logical NOT | !X is false. |
Bitwise Operators
Section titled “Bitwise Operators”Bitwise operators work on integers at the binary (bit) level. Assume A = 60 (00111100) and B = 13 (00001101):
| Operator | Description | Example (Binary -> Decimal) |
|---|---|---|
| & | Bitwise AND | A & B (00001100) -> 12 |
| | | Bitwise OR | A | B (00111101) -> 61 |
| ^ | Bitwise XOR | A ^ B (00110001) -> 49 |
| &^ | Bit clear (AND NOT) | A &^ B (00110000) -> 48. Clears bits in A that are set in B. |
| << | Left shift | A << 2 (11110000) -> 240. Shifts bits of A left by 2 positions. |
| >> | Right shift | A >> 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.
Assignment Operators
Section titled “Assignment Operators”| Operator | Description | Example |
|---|---|---|
| = | Simple assignment | C = A + B assigns value of A + B to C |
| += | Add and assign | C += A is equivalent to C = C + A |
| -= | Subtract and assign | C -= A is equivalent to C = C - A |
| *= | Multiply and assign | C *= A is equivalent to C = C * A |
| /= | Divide and assign | C /= A is equivalent to C = C / A |
| %= | Modulus and assign | C %= A is equivalent to C = C % A |
| <<= | Left shift and assign | C <<= 2 is equivalent to C = C << 2 |
| >>= | Right shift and assign | C >>= 2 is equivalent to C = C >> 2 |
| &= | Bitwise AND and assign | C &= 2 is equivalent to C = C & 2 |
| |= | Bitwise OR and assign | C |= 2 is equivalent to C = C | 2 |
| ^= | Bitwise XOR and assign | C ^= 2 is equivalent to C = C ^ 2 |
| &^= | Bit clear and assign | C &^= 2 is equivalent to C = C &^ 2 |
Miscellaneous Operators
Section titled “Miscellaneous Operators”| Operator | Description | Example |
|---|---|---|
| & | 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 operator | Used for sending and receiving values on channels (e.g., ch <- value to send, value := <-ch to receive). |
Operator Precedence in Go
Section titled “Operator Precedence in Go”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:
- Unary Operators:
+ - ! ^ & * <-(highest precedence) - Multiplicative Operators:
* / % << >> & &^ - Additive Operators:
+ - | ^ - Comparison Operators:
== != < <= > >= - Logical AND:
&& - 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.
Example of Operator Usage
Section titled “Example of Operator Usage”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") }}Go - Decision Making
Section titled “Go - Decision Making”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:
| Statement | Description |
|---|---|
if statement | Executes a block of code if a condition is true. |
if...else statement | Executes one block of code if a condition is true, and another block if it’s false. |
if...else if...else statement | Tests multiple conditions in sequence. |
switch statement | Compares an expression against a list of possible values (cases) or evaluates types. |
select statement | Specifically used with channels for concurrent operations, waits on multiple channel operations. |
Common Feature: Initialization Statement
Section titled “Common Feature: Initialization Statement”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.
if statement
Section titled “if 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)}if...else statement
Section titled “if...else statement”Syntax:
if optional_initialization_statement; condition { // block for true condition} else { // block for false condition}if...else if...else statement
Section titled “if...else if...else statement”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}switch statement
Section titled “switch statement”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)}select statement
Section titled “select statement”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.
Go - Loops
Section titled “Go - Loops”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.
The for loop
Section titled “The for loop”Go’s for loop has several forms:
- The complete
forloop (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}- The
whileloop equivalent:for condition { ... }The initialization and post statements are omitted.
n := 0for n < 5 { fmt.Println(n) n++} // Prints 0, 1, 2, 3, 4- The infinite loop:
for { ... }The condition is omitted, creating an infinite loop. Must be exited usingbreakorreturn.
sum := 0for { sum++ if sum > 10 { break // Exit the loop }} // sum becomes 11- The
for...rangeloop: 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 slicenums := []int{10, 20, 30}for index, value := range nums { fmt.Printf("Index: %d, Value: %d\n", index, value)}
// Iterating over a mapcolors := map[string]string{"red": "#ff0000", "green": "#00ff00"}for key, value := range colors { fmt.Printf("Key: %s, Value: %s\n", key, value)}Loop Control Statements
Section titled “Loop Control Statements”Go provides statements to control the flow within loops:
| Control Statement | Description |
|---|---|
break | Terminates the innermost for, switch, or select statement immediately. |
continue | Skips the remainder of the current iteration of the innermost for loop and proceeds to the next iteration (evaluating the post statement if present). |
goto | Transfers 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)}Nested Loops
Section titled “Nested Loops”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)Go - Functions
Section titled “Go - Functions”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.
Defining a Function
Section titled “Defining a Function”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:
funckeyword: 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.
returnstatement: Used to return values from the function. If a function has defined return types, it must end with areturnstatement (unless it panics or has an infinite loop).
Example: A simple max function
// max returns the larger of two integersfunc max(num1, num2 int) int { var result int if num1 > num2 { result = num1 } else { result = num2 } return result}Calling a Function
Section titled “Calling a Function”To use a function, you call it by its name, providing the required arguments.
package main
import "fmt"
// max returns the larger of two integersfunc 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}Multiple Return Values
Section titled “Multiple Return Values”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 zerofunc 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) }}Named Return Values
Section titled “Named Return Values”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 5While this feature exists, it’s often clearer to be explicit with return statements unless the function is very short.
Variadic Functions
Section titled “Variadic Functions”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 integersfunc 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 copyfunc noChange(val int) { val = 100}
// Modifies original 'val' by using a pointerfunc 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.
Functions as Values and Closures
Section titled “Functions as Values and Closures”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 namefunc 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!}Methods
Section titled “Methods”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 Circlefunc (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}Go - Scope Rules
Section titled “Go - Scope Rules”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.
Levels of Scope
Section titled “Levels of Scope”- Universe Block: Encompasses all Go source text. Identifiers declared here are predeclared (e.g.,
true,false,nil, built-in types likeint,string, built-in functions likemake,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 makefmtavailable 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 anyelseorelse ifblocks associated with anif).
Local Variables
Section titled “Local Variables”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}Package-Level (Global) Variables
Section titled “Package-Level (Global) Variables”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)}Shadowing
Section titled “Shadowing”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.
Formal Parameters
Section titled “Formal Parameters”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'}Zero Values and Initialization
Section titled “Zero Values and Initialization”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.
Go - Arrays
Section titled “Go - Arrays”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.
Declaring Arrays
Section titled “Declaring Arrays”Syntax: var arrayName [size]elementType
// Declare an array of 5 integersvar numbers [5]int
// Declare an array of 3 stringsvar names [3]stringInitializing Arrays
Section titled “Initializing Arrays”Arrays can be initialized using an array literal:
// Initialize with valuesvar 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 initializationcustomInit := [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.
Accessing Array Elements
Section titled “Accessing Array Elements”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) }}Multi-dimensional Arrays
Section titled “Multi-dimensional Arrays”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]) } }}Passing Arrays to Functions
Section titled “Passing Arrays to Functions”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 arrayfunc 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 itfunc 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.
Go - Pointers
Section titled “Go - Pointers”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.
Understanding Memory Addresses
Section titled “Understanding Memory Addresses”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}Declaring Pointers
Section titled “Declaring Pointers”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 nilvar sPtr *string // sPtr is a pointer to a string, currently nilUsing Pointers
Section titled “Using Pointers”- Assigning an address to a pointer: Use the
&operator. - 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)}nil Pointers
Section titled “nil Pointers”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 to Structs
Section titled “Pointers to Structs”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 1pv.X = 100fmt.Println(v.X) // Prints 100 (v was modified)Array of Pointers
Section titled “Array of Pointers”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]}Pointer to Pointer (Double Pointer)
Section titled “Pointer to Pointer (Double Pointer)”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)}Passing Pointers to Functions
Section titled “Passing Pointers to Functions”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 new() function
Section titled “The new() function”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 0fmt.Println(*ptr) // 0*ptr = 100fmt.Println(*ptr) // 100While new() is available, make() is more commonly used for slices, maps, and channels as it initializes their internal structures, not just zeroing memory.
Go - Structures
Section titled “Go - Structures”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.
Defining a Structure
Section titled “Defining a Structure”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 structtype Book struct { Title string Author string Subject string BookID int Pages int IsPublished bool}Creating and Initializing Structs
Section titled “Creating and Initializing Structs”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 Bookbook1.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 = ...Accessing Struct Members
Section titled “Accessing Struct Members”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 as Function Arguments
Section titled “Structs as Function Arguments”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 Bookfunc 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 and Fields
Section titled “Anonymous Structs and Fields”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}Go - Slices
Section titled “Go - Slices”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:
- A pointer to the underlying array.
- The length of the segment of the array that the slice contains.
- The capacity of the segment (maximum length it can reach by re-slicing).
Defining and Initializing Slices
Section titled “Defining and Initializing Slices”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 lengthslice1 := 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 sliceunderlyingArray := [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() and cap() functions
Section titled “len() and cap() functions”len(s): Returns the number of elements currently in the slices.cap(s): Returns the capacity of the slices(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}Nil Slices vs. Empty Slices
Section titled “Nil Slices vs. Empty Slices”- 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{}ors2 := make([]int, 0)
For most operations, nil and empty slices behave similarly (e.g., len is 0, range loop does nothing).
Slicing (Creating Sub-Slices)
Section titled “Slicing (Creating Sub-Slices)”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)}append() function
Section titled “append() function”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]copy() function
Section titled “copy() function”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]}Go - Range
Section titled “Go - Range”The range keyword is used in a for loop to iterate over elements of various data structures like arrays, slices, strings, maps, and channels.
How range Works with Different Types
Section titled “How range Works with Different Types”The range keyword returns one or two values depending on the collection being iterated:
| Collection Type | First 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 { ... }
Example: Iterating with range
Section titled “Example: Iterating with range”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: 10Index: 1, Value: 20Index: 2, Value: 30Index: 3, Value: 40Sum of numbers: 100
Iterating over map 'countryCapitalMap':Capital of France is ParisCapital of Italy is RomeCapital of Japan is Tokyo
Countries in map:FranceItalyJapan
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]Go - Recursion
Section titled “Go - Recursion”Recursion is a programming technique where a function calls itself to solve a problem. A recursive function typically has two parts:
- Base Case(s): One or more conditions that stop the recursion, preventing an infinite loop.
- 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.
Example 1: Factorial Calculation
Section titled “Example 1: Factorial Calculation”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 2432902008176640000Example 2: Fibonacci Series
Section titled “Example 2: Fibonacci Series”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 and Cons of Recursion
Section titled “Pros and Cons of Recursion”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 - Type Casting (Type Conversion)
Section titled “Go - Type Casting (Type Conversion)”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).
Syntax for Type Conversion
Section titled “Syntax for Type Conversion”The syntax for converting a value v to a type T is T(v).
var myInt int = 10var 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 assignmentvar myIntVal int = int(myFloatVal) // Convert float64 to int (truncates decimal part)Common Type Conversions
Section titled “Common Type Conversions”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) orfmt.Sprintf(). - String to Integer: Use
strconv.Atoi()(ASCII to Integer) orstrconv.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)}Type Conversions vs. Type Assertions
Section titled “Type Conversions vs. Type Assertions”It’s important to distinguish type conversions from type assertions:
- Type Conversion (
T(v)): Creates a new value of typeTfrom valuev. 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 valuevisTand extracts the underlying value of typeT. 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 - Error Handling
Section titled “Go - Error Handling”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.
The error Type
Section titled “The error Type”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.
Returning and Checking Errors
Section titled “Returning and Checking Errors”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) }}Creating Errors
Section titled “Creating Errors”Go provides several ways to create error values:
errors.New(message string) error: From theerrorspackage. 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}fmt.Errorf(format string, args ...interface{}) error: From thefmtpackage. Creates an error with a formatted message, similar tofmt.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}- Custom Error Types: You can define your own struct type that implements the
errorinterface. This allows you to include more contextual information in your errors.
package main
import "fmt"
// MyError is a custom error typetype 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) } }}Error Wrapping (Go 1.13+)
Section titled “Error Wrapping (Go 1.13+)”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.Errorfwith the%wverb wraps an error.errors.Unwrap(err error) errorretrieves the wrapped error.errors.Is(err, target error) boolchecks iferr(or any error in its chain) matchestarget.errors.As(err error, target interface{}) boolchecks iferr(or any error in its chain) is of a specific custom type and assigns it totarget.
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) } }}panic and recover
Section titled “panic and recover”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.recoveris only useful inside deferred functions. If the current goroutine is not panicking,recoverreturnsnil.
Idiomatic Go strongly prefers explicit error returns over panic for most error conditions.