What every beginner must understand before writing real Go programs.
Go programs follow a strict structure. Understanding these three pieces (package declaration, imports, and the main() function) unlocks everything else in the language.
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
Try it: https://go.dev/play/p/4Yay9nVC13E
Every runnable Go program contains these three elements. Let’s examine each one.
Every Go file begins with a package name:
package main
This tells Go what kind of file you’re creating.
What it means: package main marks this file as part of an executable program. The main package is special. It’s the only package that creates a runnable binary.
If you’re building a library or reusable code, you use a different package name:
package mathutils
func Add(a, b int) int {
return a + b
}
This code becomes a library that other programs can import. It’s not runnable on its own.
Under the hood: The Go compiler treats package main differently from all other packages. When you run go build, it searches for a main package and a main() function within it. This combination creates the entry point for executable linking. All other packages compile to object files (.a archives) that get linked into the final binary or imported by other code.
Imports bring in code from other packages:
import "fmt"
fmt is Go’s formatting and printing library. Without this import, fmt.Println() won’t work.
Multiple imports use parentheses:
import (
"fmt"
"time"
)
Try it: https://go.dev/play/p/agALnPZ_Oo8
Important: Go refuses to compile if you import a package you don’t use. This keeps your code clean:
import "fmt"
import "time" // ERROR: imported and not used
func main() {
fmt.Println("Hello!")
}
Most editors automatically remove unused imports when you save.
Under the hood: Import paths are resolved by the Go toolchain using Go modules (modern) or $GOPATH (legacy). Standard library packages like fmt and time are built into the Go distribution. No network access needed. Third-party imports use full repository paths: import "github.com/user/repo/package". The go.mod file tracks dependencies and versions. At compile time, the linker only includes the specific functions you actually call from imported packages, not the entire package. This keeps binaries small.
This is where execution begins:
func main() {
fmt.Println("Program starts here")
}
Every executable Go program must have a main() function inside a main package. Go starts running your code from the first line inside main().
No main() function means no executable program.
Try it: https://go.dev/play/p/ULZzsq91me9
Under the hood: Unlike C, Go’s main() takes no arguments and returns no value. The function signature is always func main(). Command-line arguments are accessed through os.Args (a string slice). Exit codes are set with os.Exit(code).
Go runs code in a predictable sequence:
package declaration → imports → main()
Code outside functions doesn’t execute automatically. You can declare package-level variables, but all logic must be inside functions.
Here’s a program showing execution order:
package main
import "fmt"
func main() {
fmt.Println("1: Starting program")
sayHello()
fmt.Println("3: Program end")
}
func sayHello() {
fmt.Println("2: Hello from another function!")
}
Output:
1: Starting program
2: Hello from another function!
3: Program end
Try it: https://go.dev/play/p/KdOs6qtlMjo
The flow is straightforward:
main()sayHello() runs completelymain()Use this as your default starting point:
package main
import "fmt"
func main() {
fmt.Println("Hello!")
}
Try it: https://go.dev/play/p/dwRzlq817qT
Every time you start a new Go program, begin with this structure. Add your code inside main(), import packages as you need them, and you’re ready to build.
How to name things correctly in Go and avoid classic beginner mistakes.
Names in Go determine visibility, meaning, and whether your code compiles. Understanding these rules prevents common errors and helps you write idiomatic Go.
An identifier is the name of a variable, function, type, constant, or package.
Valid identifier rules:
_)Valid examples:
count
age2
_x
customerName
Invalid examples:
2people // starts with a number
first-name // hyphens not allowed
var // keyword
Try it: https://go.dev/play/p/lu_J_pT1ZBx
Go’s parser distinguishes identifiers from numbers by the first character.
These are invalid:
1abc
3total
Underscores are allowed but discouraged:
_var := 5 // valid but not idiomatic
Under the hood: The blank identifier _ is special—it’s a write-only identifier that discards values without creating a binding. Leading underscores in regular identifiers are typically used to signal “ignore this” or to export test helpers that shouldn’t be used in production code. Some linters flag leading underscores as potential code smells.
Keywords have special meaning and cannot be used as names.
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
Try using one as a variable: https://go.dev/play/p/XHYbfgQHTUM
The compiler rejects it immediately.
Go has built-in identifiers that aren’t keywords:
len, cap, append, makestring, int, error, booltrue, false, nilYou can shadow them, but you shouldn’t:
len := 10 // now len() is inaccessible
Try it: https://go.dev/play/p/miLbdI_mcAF
You’ll get confusing errors when you try to call the built-in function.
Go has strong conventions for package names.
The rules:
utilsStandard library examples:
strings
http
json
sort
math
io
time
Good custom examples:
auth
storage
billing
user
email
config
Avoid:
myPackage
helpers
utility_stuff
Under the hood: Package names become part of the API—clients write http.Get(), not Get(). The name appears at every call site, so brevity matters. The convention avoids stutter: user.UserService is redundant; use user.Service. Import paths can be long (github.com/company/project/user), but the package name (the last path element) should be short. The compiler uses package names for symbol resolution, and documentation tools use them to generate indexes.
Go uses capitalization for access control. No public or private keywords.
Example:
type User struct {} // exported type
func Print() {} // exported function
var name string // unexported
func calculate() {} // unexported
Go uses specific casing conventions:
camelCase for unexported identifiersPascalCase for exported identifierslowercase for package namesSNAKE_CASE rarely (only for constant groups when needed)Examples:
package billing
type Invoice struct{} // PascalCase (exported)
func calculateTotal() {} // camelCase (unexported)
func FormatTotal() {} // PascalCase (exported)
var accountID = 123 // camelCase
Constants use PascalCase:
const Pi = 3.14
const MaxConnections = 100
How to store and manipulate data in your Go programs.
Variables let programs remember values, perform calculations, and track state. Go keeps variables simple but strict. Every variable has a type that never changes.
The standard way to declare a variable:
var age int
age = 30
This declares age as an integer. You assign the value separately.
You can declare and initialize together:
var name string = "Sebastian"
Try it: https://go.dev/play/p/7U7dDH9JcRf
Inside functions, use the shorter form:
count := 10
This declares the variable and infers its type from the value.
This is how most Go code creates variables:
message := "Hello"
pi := 3.14
active := true
Try it: https://go.dev/play/p/6dbAPsDN0X0
Go determines types automatically from values:
x := 42 // int
y := 3.14 // float64
z := "hello" // string
You can specify types explicitly when needed:
var score int = 100
Most Go code relies on inference for cleaner syntax.
Under the hood: Go’s type inference system resolves types at compile time with no runtime cost. Untyped constants like 42 have default types (int for integers, float64 for floats, complex128 for complex numbers). The inference algorithm considers the entire expression. When assigning to an interface, the concrete type is inferred and the interface holds both the type and value. Generic functions use constraint-based inference to determine type parameters.
Variables declared without initialization receive default zero values.
Common zero values:
TypeZero Valueint0float640.0string"" (empty string)boolfalsepointer, map, slice, func, interfacenil
Example:
var n int
var text string
var ok bool
fmt.Println(n) // 0
fmt.Println(text) // ""
fmt.Println(ok) // false
Try it: https://go.dev/play/p/I5_CmBeBuLS
Go supports several declaration forms.
Single line:
var a, b, c int
With values:
x, y := 10, 20
Block form:
var (
firstName string
age int
active bool
)
Try it: https://go.dev/play/p/cv0CO-T5ie2
Mixed types:
name, score := "Alice", 99
Variables can be read and changed after declaration:
count := 1
fmt.Println(count) // 1
count = 2
fmt.Println(count) // 2
count = count + 5
fmt.Println(count) // 7
Try it: https://go.dev/play/p/_wfwxixWA0t
Variables hold state throughout your program.
Understanding the four core types you’ll use in every Go program.
Go keeps its type system simple. Four types handle most programming tasks: integers, floats, strings, and booleans. These types appear everywhere: variables, function parameters, return values, and data structures.
The int type represents whole numbers without decimals.
age := 30
score := -5
count := 0
Basic arithmetic works as expected:
a := 10
b := 3
fmt.Println(a + b) // 13
fmt.Println(a - b) // 7
fmt.Println(a * b) // 30
fmt.Println(a / b) // 3 (integer division)
fmt.Println(a % b) // 1 (remainder)
Try it: https://go.dev/play/p/OkWhPjylJxJ
Important: Division between integers truncates. 10 / 3 equals 3, not 3.333...
Under the hood: int is platform-dependent. On 64-bit systems it’s 64 bits, on 32-bit systems it’s 32 bits. This matches the native word size for optimal performance. Go also provides sized variants (int8, int16, int32, int64) for specific needs like binary protocols or memory-constrained environments. Unsigned versions (uint, uint8, etc.) exist for values that are never negative. Integer overflow wraps around silently rather than raising errors.
The float64 type handles decimal numbers.
pi := 3.14159
temperature := -2.5
ratio := 0.75
Decimal arithmetic:
x := 5.0
y := 2.0
fmt.Println(x / y) // 2.5
fmt.Println(x * y) // 10.0
Try it: https://go.dev/play/p/8hz1fADjfh5
Important: You cannot mix int and float64 in operations without explicit conversion:
result := float64(5) + 3.2 // must convert int to float64
A string holds text enclosed in double quotes:
name := "Sebastian"
greeting := "Hello, Go!"
empty := ""
Strings in Go are immutable. You cannot change individual characters.
Common operations:
fmt.Println(len("hello")) // 5 (length in bytes)
fmt.Println("go" + "lang") // "golang" (concatenation)
fmt.Println("hello"[0]) // 104 (byte value of 'h')
Try it: https://go.dev/play/p/PKEBHzN2Dal
Important: Always use double quotes " " for strings. Single quotes ' ' are for runes (single characters).
The bool type has exactly two values:
isLoggedIn := true
hasPermission := false
Booleans come from comparisons and logical operations:
age := 20
isAdult := age >= 18
fmt.Println(isAdult) // true
Common comparison operators:
x := 10
y := 20
fmt.Println(x == y) // false (equal)
fmt.Println(x != y) // true (not equal)
fmt.Println(x < y) // true (less than)
fmt.Println(x > y) // false (greater than)
fmt.Println(x <= y) // true (less or equal)
fmt.Println(x >= y) // false (greater or equal)
Logical operators:
fmt.Println(true && false) // false (AND)
fmt.Println(true || false) // true (OR)
fmt.Println(!true) // false (NOT)
Try it: https://go.dev/play/p/ZK9bYBlfGok
Under the hood: Booleans occupy one byte in memory despite only needing one bit. This is a space/speed tradeoff. Byte-aligned values are faster to access on most architectures. The compiler optimizes boolean expressions using short-circuit evaluation. In a && b, if a is false, b never executes. In a || b, if a is true, b never executes.
Go doesn’t use exceptions. Functions return an error value to indicate failure.
Common pattern:
result, err := doSomething()
if err != nil {
// handle the error
fmt.Println("Error:", err)
return
}
// use result
Try a failing conversion: https://go.dev/play/p/Y4XjyWiLC3C
Key points:
nil means no error (success)Go requires explicit conversion between types. There’s no automatic coercion.
Integer to float:
x := 5
y := float64(x) + 3.2 // must convert
Float to integer (truncates):
z := 3.9
w := int(z) // w = 3
String to int:
value, err := strconv.Atoi("42")
Mixing int and float without conversion:
result := 5 + 3.2 // ERROR: cannot mix types
Using single quotes for strings:
name := 'hello' // ERROR: runes, not strings
Comparing floats with ==:
if 0.1 + 0.2 == 0.3 {} // may fail due to rounding
The simplest and most effective way to understand what your Go program is doing.
Printing lets you see what your code is doing. The fmt package provides tools to print values, format them, or build strings for later use. These four functions handle most printing needs: Print, Println, Printf, and Sprintf.
fmt.Println prints values with spaces between arguments and adds a newline at the end.
fmt.Println("Hello")
fmt.Println("Age:", 30)
fmt.Println(1, 2, 3)
Output:
Hello
Age: 30
1 2 3
Try it: https://go.dev/play/p/Eep61FJfl8q
Use this when you want clean, simple output.
fmt.Print works like Println but does not add a newline.
fmt.Print("Hello")
fmt.Print(" ")
fmt.Print("World")
Output:
Hello World
Try it: https://go.dev/play/p/HhxyXo2b5aO
Use this when you want full control over spacing and line breaks.
fmt.Printf gives you fine-grained control with format verbs.
Common format verbs:
%v - default format (prints any value)%s - string%d - integer%f - float%t - bool%T - type of valueExample:
name := "Alice"
age := 25
height := 1.68
fmt.Printf("Name: %s\n", name)
fmt.Printf("Age: %d\n", age)
fmt.Printf("Height: %f\n", height)
fmt.Printf("Any value: %v\n", age)
Output:
Name: Alice
Age: 25
Height: 1.680000
Try it: https://go.dev/play/p/IW_8qhDyqLq
You must include \n if you want a newline. Printf does not add one automatically.
Sprintf works like Printf but returns the formatted string instead of printing it.
name := "Bob"
msg := fmt.Sprintf("Hello, %s!", name)
fmt.Println(msg) // prints: Hello, Bob!
Use this when you want to build a string first, then use it elsewhere.
Try it: https://go.dev/play/p/iaAeumq24-DD
Another example:
age := 40
info := fmt.Sprintf("Age = %d", age)
fmt.Println(info)
By default, %f shows six decimal places. Control this with precision modifiers.
pi := 3.14159265
fmt.Printf("%.2f\n", pi) // 3.14
fmt.Printf("%.4f\n", pi) // 3.1416
fmt.Printf("%f\n", pi) // 3.141593 (default 6)
The number after the dot specifies decimal places.
Use %T to see a value’s type:
x := 42
y := 3.14
z := "hello"
fmt.Printf("Type of x: %T\n", x) // int
fmt.Printf("Type of y: %T\n", y) // float64
fmt.Printf("Type of z: %T\n", z) // string
Try it: https://go.dev/play/p/4mYO7ircTuD
This is extremely helpful when learning Go’s type system.
Use Println when:
Use Print when:
Use Printf when:
Use Sprintf when:
Print everything with %v:
fmt.Printf("x = %v\n", x)
Print with type information:
fmt.Printf("x = %v (type %T)\n", x, x)
Print multiple values:
fmt.Printf("x=%v, y=%v, z=%v\n", x, y, z)
Quick debug multiple variables:
fmt.Println("x:", x, "y:", y, "z:", z)
How your program makes decisions based on conditions.
Conditionals let programs choose different paths. They run code only when conditions are true. Go keeps conditionals simple and explicit. No tricks, no surprises.
An if statement runs code only when a condition is true:
age := 20
if age >= 18 {
fmt.Println("You are an adult.")
}
If the condition is false, the code inside the braces is skipped.
Try it: https://go.dev/play/p/9jwto6S8d3n
The else clause runs when the condition is false:
age := 15
if age >= 18 {
fmt.Println("Adult")
} else {
fmt.Println("Minor")
}
Exactly one branch executes. Never both.
Try it: https://go.dev/play/p/nM6ZhD_RsTK
Chain multiple conditions to check them in order:
score := 72
if score >= 90 {
fmt.Println("Grade A")
} else if score >= 75 {
fmt.Println("Grade B")
} else if score >= 60 {
fmt.Println("Grade C")
} else {
fmt.Println("Fail")
}
Go checks conditions from top to bottom. The first true condition wins. The rest are skipped.
Try it: https://go.dev/play/p/bLVSmSUVWYQ
Go requires conditions to be boolean expressions. These are invalid:
if 1 { // ERROR: 1 is not bool
fmt.Println("nope")
}
if "text" { // ERROR: string is not bool
fmt.Println("nope")
}
if x { // ERROR if x is int, string, etc.
fmt.Println("nope")
}
Try it to see the error: https://go.dev/play/p/c99lMsjZtGe
You must compare explicitly:
if x > 0 { // valid: comparison returns bool
fmt.Println("positive")
}
if name == "Bob" { // valid: equality returns bool
fmt.Println("Hi Bob")
}
if ok { // valid only if ok is bool
fmt.Println("success")
}
Go lets you run a statement before the condition:
if n := len("hello"); n > 3 {
fmt.Println("Long string")
}
The variable n exists only inside the if block (and any else blocks).
Try it: https://go.dev/play/p/PsivFE953Fm
This is common for error checking:
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
return
}
Under the hood: The short statement syntax creates a new scope. The variable declared in the statement is visible only within the if/else blocks. The compiler allocates the variable once, before evaluating the condition. If the variable escapes (unlikely in most if statements), it may be heap-allocated. Otherwise, it lives on the stack. This pattern reduces variable clutter in the outer scope. It’s especially useful for error handling because the error variable doesn’t pollute the function scope. The semicolon separates the statement from the condition. The compiler parses this as: run statement, then evaluate condition using any variables the statement declared.
Check a boolean variable:
if isLoggedIn {
fmt.Println("Welcome!")
}
Compare strings:
if role == "admin" {
fmt.Println("Access granted")
}
Compare numbers:
if temperature < 0 {
fmt.Println("Freezing")
}
Check for errors:
if err != nil {
fmt.Println("Something went wrong")
return
}
Validate input:
if age < 0 || age > 120 {
fmt.Println("Invalid age")
return
}
Use logical operators to combine conditions:
age := 25
hasLicense := true
if age >= 18 && hasLicense {
fmt.Println("Can drive")
}
Logical operators:
&& - AND (both must be true)|| - OR (at least one must be true)! - NOT (inverts the boolean)Examples:
if x > 0 && x < 100 {
fmt.Println("x is between 1 and 99")
}
if role == "admin" || role == "moderator" {
fmt.Println("Has elevated permissions")
}
if !isEmpty {
fmt.Println("Contains data")
}
Under the hood: Logical operators use short-circuit evaluation. In a && b, if a is false, b never executes. In a || b, if ais true, b never executes. This is both a performance optimization and a safety feature. It prevents errors like if x != nil && x.Value > 0 where the second condition would panic if the first is false. The compiler generates conditional branches for each operator. For &&, a false left side jumps past the right side. For ||, a true left side jumps past the right side. Complex boolean expressions are evaluated left-to-right with the minimum number of comparisons needed to determine the result.
Use if when your program needs to:
React to user input:
if command == "quit" {
return
}
Validate data:
if email == "" {
fmt.Println("Email required")
}
Handle error cases:
if err != nil {
log.Fatal(err)
}
Implement business rules:
if balance < withdrawAmount {
fmt.Println("Insufficient funds")
}
Branch into different behaviors:
if isPremiumUser {
showPremiumFeatures()
} else {
showBasicFeatures()
}
Conditionals are the foundation of program logic. You’ll use them everywhere.
Using non-boolean conditions:
if 1 { // ERROR: must be bool
}
Assignment instead of comparison:
if x = 5 { // ERROR: assignment doesn't return bool
}
Accessing short-statement variable outside scope:
if n := len(s); n > 0 {
fmt.Println(n) // OK
}
fmt.Println(n) // ERROR: n not defined here
How to break your program into small, reusable pieces.
Functions let you organize code, avoid repetition, and make programs easier to understand. They group related logic under a single name. Go keeps functions simple and explicit. No hidden behavior, no magic.
A function has four parts:
Example:
func greet() {
fmt.Println("Hello!")
}
You call it like this:
greet()
Try it: https://go.dev/play/p/ThIQg5YlP5U
Parameters let you pass information into a function.
func greet(name string) {
fmt.Println("Hello,", name)
}
greet("Sebastian")
Try it: https://go.dev/play/p/J6luIVFH1yH
Multiple parameters:
func add(a int, b int) {
fmt.Println(a + b)
}
When parameters share a type, you can group them:
func add(a, b int) {
fmt.Println(a + b)
}
Functions can return a value:
func add(a, b int) int {
return a + b
}
result := add(3, 4)
fmt.Println(result) // 7
Try it: https://go.dev/play/p/S4QPyd7pSYR
The return type comes after the parameters. The return keyword sends the value back to the caller.
Go supports returning multiple values. This is commonly used for error handling.
Example:
func divide(a, b float64) (float64, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
Usage:
result, ok := divide(10, 2)
fmt.Println(result, ok) // 5 true
Try it: https://go.dev/play/p/wY43lpBipzR
The second value indicates success or failure. This pattern appears throughout Go code.
Sometimes you don’t need all return values. Go requires you to explicitly ignore them with _.
Example:
result, _ := divide(10, 0)
fmt.Println(result)
The _ means “I don’t need this value.”
Try it: https://go.dev/play/p/OCy6gzFWeF3
This is common when temporarily ignoring errors during development. Production code should check errors.
Under the hood: The blank identifier is a write-only sink. It discards values without creating bindings. The compiler recognizes _ and generates no code for assignments to it. This is pure syntax. Using _ prevents “unused variable” errors while making your intent explicit. You’re saying “I know this returns two values, and I’m intentionally ignoring the second.”
You can name return values in the function signature:
func divide(a, b float64) (result float64, ok bool) {
if b == 0 {
return 0, false
}
result = a / b
ok = true
return
}
Named returns are initialized to their zero values. A bare return sends back the current values of named returns.
Create a function when:
Good example:
func isAdult(age int) bool {
return age >= 18
}
Bad example: A 50-line main() function with repeated checks.
Small, well-named functions make code clean, readable, and testable.
Lowercase function names are private to the package:
func calculateTotal() int {
return 100
}
Uppercase function names are public (exported):
func CalculateTotal() int {
return 100
}
Error handling:
value, err := someFunction()
if err != nil {
return err
}
// use value
Success flag:
result, ok := tryOperation()
if !ok {
// handle failure
}
Multiple related values:
x, y, z := getCoordinates()
Under the hood: These patterns are idiomatic because they align with Go’s design philosophy. Explicit error handling prevents silent failures. The if err != nil check appears frequently but makes error paths visible in the code. Unlike exceptions, errors don’t hide control flow. The success flag pattern (ok) is used throughout the standard library (map lookups, type assertions, channel receives). It provides a lightweight way to signal success or failure without allocating error objects.
How to group related data together into meaningful units.
Structs let you bundle related data under a single name. They’re the foundation of most real-world Go programs and appear throughout the standard library. A struct is like a container holding named fields that belong together.
You define a struct type using type followed by the struct name and its fields:
type Person struct {
Name string
Age int
}
This creates a new type you can use anywhere in your program.
Try it: https://go.dev/play/p/ycv8-eDNxHy
The struct has two fields: Name (a string) and Age (an integer).
Under the hood: Struct definitions are compile-time constructs. The compiler calculates the struct’s size and field offsets when processing the type definition. Fields are laid out in memory in declaration order. The compiler may add padding between fields to satisfy alignment requirements. On 64-bit systems, the Person struct occupies 24 bytes: 16 bytes for the string (pointer + length) and 8 bytes for the int, though padding may adjust this. Struct types become part of the type system. The compiler tracks them for type checking and generates field access code using constant offsets.
There are several ways to create structs. The most common uses a struct literal with field names:
p := Person{
Name: "Alice",
Age: 30,
}
This explicitly names each field. The order doesn’t matter.
You can also create an empty struct and assign fields later:
var p Person
p.Name = "Bob"
p.Age = 25
Or use positional syntax (not recommended):
p := Person{"Charlie", 40}
This relies on field order and breaks if you reorder fields in the struct definition.
Try the examples: https://go.dev/play/p/qJj7q9578lH
Under the hood: Struct literals compile to stack allocations (if the struct doesn’t escape) or heap allocations (if it does). Named field syntax (Name: "Alice") is safer because the compiler reorders assignments to match the struct’s field order at compile time. Positional syntax creates ordering dependencies that become bugs when structs evolve. Zero-valued struct creation (var p Person) is free. The memory is zeroed during allocation. The compiler optimizes away explicit zero initialization since the memory is already zeroed. Struct literals with some fields omitted initialize missing fields to their zero values automatically.
Use the dot . operator to read struct fields:
fmt.Println(p.Name)
fmt.Println(p.Age)
The syntax is simple and explicit. You always know what you’re accessing.
Struct fields can be modified directly:
p.Age = 31
p.Name = "Alice Smith"
Any field can be changed. Structs are mutable by default.
Try updating: https://go.dev/play/p/zdJbeEE-c4y
Structs can be function parameters just like basic types:
func printPerson(p Person) {
fmt.Println(p.Name, p.Age)
}
printPerson(p)
Try it: https://go.dev/play/p/I-3z06KaivQ
Go passes structs by value. The function receives a copy:
func growUp(p Person) {
p.Age++
}
growUp(p)
fmt.Println(p.Age) // unchanged
Try it: https://go.dev/play/p/ufduPbXCxqP
The original struct is not modified because the function works on a copy.
When you create a struct without initialization, all fields get their zero values:
var p Person
fmt.Println(p.Name) // "" (empty string)
fmt.Println(p.Age) // 0
This is safe. You never have undefined struct fields.
Under the hood: Struct zero values are recursive. Each field is initialized to its own type’s zero value. Strings become empty, integers become zero, booleans become false, pointers become nil, nested structs are recursively zeroed, and slices/maps/channels become nil. The memory is zeroed during allocation (either stack or heap). The compiler can often eliminate explicit zero initialization since the allocator already zeroed the memory. This “zero value is useful” principle makes var p Person immediately safe to use for many types, even without explicit initialization.
Even if you don’t create complex structs immediately, you need to understand them because:
Many standard library APIs use them:
http.Request
json.Decoder
time.Time
JSON decoding uses structs:
type Config struct {
Port int
Host string
}
Configuration and options use them:
type ServerOptions struct {
Timeout time.Duration
MaxConnections int
}
They represent real-world data naturally:
type User struct {
ID int
Username string
Email string
}
Structs are fundamental to Go programming. You’ll see them everywhere.
Grouping configuration:
type AppConfig struct {
Debug bool
Port int
Host string
}
config := AppConfig{
Debug: true,
Port: 8080,
Host: "localhost",
}
Representing domain objects:
type Product struct {
ID int
Name string
Price float64
}
API request/response types:
type LoginRequest struct {
Username string
Password string
}
type LoginResponse struct {
Token string
UserID int
}
Under the hood: These patterns are idiomatic in Go because structs provide clear, self-documenting data structures. Configuration structs make options explicit and type-safe. Domain structs map directly to database tables or API schemas. Request/response structs make API contracts explicit in code. The compiler enforces that all required fields exist. Tools like json tags enable automatic serialization. The pattern config := AppConfig{} creates a zero-valued struct, which often serves as sensible defaults. This eliminates the need for constructor functions in many cases.
Understanding memory addresses and how to modify values through pointers.
Pointers sound scary at first, but Go’s version is simple. You only need a light understanding so that types like *os.File, *bytes.Buffer, or *http.Client don’t confuse you. This section covers exactly the basics. No pointer arithmetic, no complex memory diagrams.
A pointer is a value that holds the memory address of another value.
Think of it like this:
Example:
x := 10
px := &x // px is a pointer to x
px doesn’t hold 10. It holds something like 0xc0000140a0 (an address in memory).
Try it: https://go.dev/play/p/kC0t-O6mylH
&value gives you a pointer to that value.
n := 42
p := &n // p points to n
You can print the address:
fmt.Println(p) // prints something like 0xc000012028
Every time you get a pointer, it came from using & or from a function that returns a pointer.
Under the hood: The & operator is called the “address-of” operator. At compile time, the compiler determines where the variable lives (stack or heap).
If p is a pointer, *p gives you the value stored at that address.
x := 5
p := &x
fmt.Println(*p) // prints 5
You can also modify the underlying value:
*p = 10
fmt.Println(x) // now x is 10
Try it: https://go.dev/play/p/yxhSN_hnLWc
This is the key idea: Changing through a pointer changes the original value.
Under the hood: The * operator is called “dereference” or “indirection.” It follows the pointer to the memory address and reads or writes the value there.
Let’s show a clear side-by-side example.
Without pointer (value copy):
func increment(n int) {
n = n + 1
}
x := 10
increment(x)
fmt.Println(x) // still 10
With pointer:
func increment(p *int) {
*p = *p + 1
}
x := 10
increment(&x)
fmt.Println(x) // now 11
Try it: https://go.dev/play/p/KYqnpfCSx1O
Because Go passes arguments by value, normal variables are copied. Pointers let you modify the original.
Pointers have types. A *int is different from a *string:
var p *int // pointer to int
var q *string // pointer to string
Pointers have a zero value: nil.
var p *int
fmt.Println(p) // <nil>
Dereferencing nil causes a panic:
var p *int
*p = 10 // PANIC: runtime error: invalid memory address
Always check for nil before dereferencing:
if p != nil {
fmt.Println(*p)
}
Under the hood: nil is represented as the address 0x0. This address is deliberately invalid. The operating system reserves the lowest memory page and marks it as inaccessible. Attempting to dereference nil causes a segmentation fault (SIGSEGV signal), which the Go runtime catches and converts to a panic. The panic includes a stack trace showing where the nil dereference occurred.
Many standard library types are large structures. You don’t want to copy them around.
Examples:
*os.File*bytes.Buffer*http.Client*regexp.RegexpThese are big objects that represent files, buffers, network clients, and compiled regex engines.
Returning a pointer means:
Example:
file, err := os.Open("test.txt")
// file is *os.File
You don’t want to copy an entire file descriptor. You use a pointer to the original.
Go provides new() to allocate memory and return a pointer:
p := new(int)
*p = 42
fmt.Println(*p) // 42
This is equivalent to:
var x int
p := &x
You’ll rarely use new() because composite literals are more common:
p := &Person{Name: "Alice", Age: 30}
Pointers are commonly used with methods to modify struct values:
type Counter struct {
Count int
}
func (c *Counter) Increment() {
c.Count++
}
func main() {
c := Counter{Count: 0}
c.Increment()
fmt.Println(c.Count) // 1
}
Without the pointer receiver, the method would work on a copy and the original would be unchanged.
Under the hood: A method with a pointer receiver (func (c *Counter)) receives a pointer to the struct. The method can modify the struct’s fields through the pointer. Go’s syntactic sugar lets you call pointer receiver methods on values (c.Increment()) or pointers ((&c).Increment()). The compiler inserts the necessary & or * operators automatically. This is different from function parameters, where you must explicitly pass &x for a pointer parameter. The pointer receiver pattern is idiomatic for any method that needs to modify the receiver or avoid copying large structs. Methods with value receivers get a copy and cannot modify the original.
You can safely understand pointers with this simplified truth:
A pointer lets a function see and change the original variable instead of a copy.
This covers 95% of cases beginners encounter.
When you see *Type in function signatures or stdlib APIs:
&variable to give it the addressWhen you see a function return *Type:
Under the hood: This mental model is accurate but simplified. The deeper reality is that Go is always pass-by-value. When you pass &x, you’re passing the value of x’s address. The function receives a copy of the address. Since the address points to the original memory location, dereferencing it accesses the original data. This distinction matters when you reassign a pointer parameter inside a function (the reassignment doesn’t affect the caller’s pointer). But for field modifications and most real-world use cases, “pointers let you modify the original” is correct and sufficient.
Modifying a value in a function:
func update(p *int) {
*p = 100
}
x := 10
update(&x)
Checking for nil before use:
if file != nil {
file.Close()
}
Working with large structs:
func process(data *LargeStruct) {
// avoid copying
}
Methods that modify state:
func (c *Counter) Reset() {
c.Count = 0
}
Under the hood: These patterns emerge from Go’s design. Pass-by-value encourages immutability by default (safe and easy to reason about). Pointers enable mutation when needed (efficient for large data). The nil check pattern prevents panics. Stdlib APIs consistently use pointers for resources (files, connections, buffers) because these represent unique kernel objects that cannot be copied. Method receivers use pointers to avoid copying structs on every method call and to enable mutation. This balance between safety (pass-by-value) and efficiency (pointers when needed) is central to Go’s design philosophy.
Dereferencing nil:
var p *int
*p = 10 // PANIC: nil pointer dereference
Forgetting & when passing pointer:
func modify(p *int) {
*p = 10
}
x := 5
modify(x) // ERROR: cannot use x (type int) as type *int
Confusing * in type vs dereference:
var p *int // * declares pointer type
fmt.Println(*p) // * dereferences the pointer
Returning pointer to local variable (unnecessary):
func makeInt() *int {
x := 42
return &x // works but causes heap allocation
}
Functions that belong to types, used everywhere in Go’s standard library.
Methods look like functions, but they “belong to” a type. You’ll see methods everywhere: file.Close(), buf.Write(), t.Run(). This section gives you enough understanding to read and use methods without needing to master object-oriented programming.
A method is a function with a receiver.
The receiver appears between the func keyword and the method name:
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Println("Hello, my name is", p.Name)
}
You call it using dot notation:
p := Person{Name: "Alice"}
p.Greet() // prints: Hello, my name is Alice
Try it: https://go.dev/play/p/6Lk_jV-KeyV
Think of p.Greet() as: “call the Greet function with p as the receiver.”
A value receiver means the method gets a copy of the value:
func (p Person) Rename(newName string) {
p.Name = newName // modifies the copy
}
Using it:
p := Person{Name: "Alice"}
p.Rename("Bob")
fmt.Println(p.Name) // still "Alice"
Try it: https://go.dev/play/p/Ah8Vgj2gChY
The original struct is unchanged. The method worked on a copy.
Value receivers are good for:
A pointer receiver means the method can modify the original value:
func (p *Person) Rename(newName string) {
p.Name = newName
}
Using it:
p := Person{Name: "Alice"}
p.Rename("Bob")
fmt.Println(p.Name) // now "Bob"
Try it: https://go.dev/play/p/D4x20PmOGQk
The original struct is modified. The method received a pointer to the original.
Pointer receivers are used when:
Go makes methods beginner-friendly with automatic conversions.
If a method has a pointer receiver:
func (p *Person) Rename(name string) { }
You can call it on a value:
p := Person{}
p.Rename("Alice") // works! Go takes &p automatically
If a method has a value receiver:
func (p Person) Greet() { }
You can call it on a pointer:
p := &Person{}
p.Greet() // works! Go dereferences automatically
Try it: https://go.dev/play/p/p0zPhVxq8vO
Go automatically handles &p and *p for method calls. This is why beginners rarely need to manually write (&p).Method() or (*p).Method().
Under the hood: This automatic conversion is compile-time syntactic sugar. When you call p.Rename("Alice") on a value with a pointer receiver method, the compiler generates code to take the address: (&p).Rename("Alice"). When you call p.Greet() on a pointer with a value receiver method, the compiler generates code to dereference: (*p).Greet(). This only works when the variable is addressable (stored in memory with an address). You cannot call pointer receiver methods on literal values or function return values directly: Person{}.Rename("X") won’t compile if Rename has a pointer receiver because the literal has no address. The automatic conversion makes Go methods feel more object-oriented while maintaining Go’s explicit nature under the hood.
Use a consistent approach for all methods on a type. Don’t mix unless you have a specific reason.
Use pointer receivers when:
*T, use it for all)Use value receivers when:
Common pattern (most types): Use pointer receivers for everything except tiny immutable types like time.Time or coordinates.
The standard library uses methods everywhere:
file.Close()
buffer.WriteString("hello")
regex.MatchString("x")
time.Now().Add(time.Hour)
server.ListenAndServe()
If you understand:
You can comfortably read 90% of Go APIs.
Methods organize related functions: Instead of CloseFile(file), you write file.Close(). This groups functions with the data they operate on.
Methods enable interfaces: Types that implement certain methods satisfy interfaces. This unlocks polymorphism and generic programming patterns.
Methods that read state:
func (p Person) GetName() string {
return p.Name
}
Methods that modify state:
func (c *Counter) Increment() {
c.Count++
}
Methods that reset state:
func (c *Counter) Reset() {
c.Count = 0
}
Methods for formatting:
func (p Person) String() string {
return fmt.Sprintf("Person{Name: %s}", p.Name)
}
Chaining methods (fluent interface):
func (b *Builder) Add(s string) *Builder {
b.data = append(b.data, s)
return b
}
// usage: builder.Add("a").Add("b").Add("c")
Method: Associated with a type, uses dot notation
func (p Person) Greet() {
fmt.Println("Hello", p.Name)
}
p.Greet()
Function: Standalone, takes explicit parameters
func Greet(p Person) {
fmt.Println("Hello", p.Name)
}
Greet(p)
Both work. Methods are preferred when:
Under the hood: At the binary level, methods and functions are nearly identical. Both compile to assembly with parameters and return values. The difference is organizational and semantic. Methods group functions with data, making code more discoverable and maintainable. The compiler uses method sets (the list of methods a type has) for interface satisfaction checks. Functions are simpler for one-off operations or when the operation doesn’t naturally belong to any single type. In Go, you cannot add methods to types from other packages, which prevents monkey-patching and keeps APIs stable.
Mixing receivers inconsistently:
func (p Person) GetName() string { } // value
func (p *Person) SetName(s string) { } // pointer
// confusing: some methods copy, others don't
Calling pointer receiver on non-addressable value:
func (p *Person) Rename(name string) { }
Person{}.Rename("X") // ERROR: cannot take address of Person{}
How Go handles failures without exceptions.
Go doesn’t use exceptions. Functions return an error value when something goes wrong. This makes failures explicit and visible in your code.
Functions that can fail return error as their last return value:
func openDatabase() error {
// if connection fails, return an error
// if everything works, return nil
}
Real example:
package main
import (
"errors"
"fmt"
)
func fail() error {
return errors.New("something went wrong")
}
func main() {
err := fail()
fmt.Println(err)
}
Try it: https://go.dev/play/p/8tUbb0zYNbC
nil means success. Anything else means failure.
This is Go’s most common error handling pattern:
package main
import (
"errors"
"fmt"
)
func mightFail(ok bool) (string, error) {
if !ok {
return "", errors.New("operation failed")
}
return "Success!", nil
}
func main() {
result, err := mightFail(false)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
}
Try it: https://go.dev/play/p/ya1aIE8vrGT
Check the error immediately. If it’s not nil, handle it before continuing.
Why this works:
Return immediately when an error occurs. Don’t nest success cases.
Good pattern:
package main
import (
"errors"
"fmt"
)
func greet(name string) (string, error) {
if name == "" {
return "", errors.New("name cannot be empty")
}
return "Hello, " + name, nil
}
func main() {
msg, err := greet("")
if err != nil {
fmt.Println("Failed:", err)
return
}
fmt.Println(msg)
}
Try it: https://go.dev/play/p/NrpeK5fEv3Z
The happy path stays unindented. Error cases exit early.
Bad pattern:
func processData(input string) error {
if input != "" {
data := parse(input)
if data != nil {
err := validate(data)
if err == nil {
return save(data)
}
}
}
return errors.New("failed")
}
This nests the happy path deeply. Avoid this style.
Ignoring errors causes bugs. Go makes errors visible so you can’t skip them accidentally.
Example showing the danger:
package main
import (
"errors"
"fmt"
)
func risky() (string, error) {
return "", errors.New("failed to get data")
}
func main() {
data, err := risky()
// ERROR IGNORED — bad practice!
fmt.Println("Data:", data)
fmt.Println("Actual error:", err)
}
Try it: https://go.dev/play/p/VFvp1SedFJ0
The code continues with empty data. In production, this causes crashes or wrong results.
The safe way:
data, err := risky()
if err != nil {
log.Fatal("Cannot get data:", err)
}
// now you know data is valid
Under the hood: When a function returns an error, it often returns zero values for other return values. For example, risky() returns "", error on failure. If you ignore the error and use the data, you’re working with an empty string (or nil slice, zero int, etc.). The blank identifier _ prevents unused variable errors but provides no safety. Linters like errcheck, golangci-lint, and staticcheck detect ignored errors. The Go community strongly discourages ignoring errors. Even debugging code should log errors rather than dropping them silently.
Use errors.New() for simple messages:
package main
import (
"errors"
"fmt"
)
func hello(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
return "Hello, " + name, nil
}
func main() {
msg, err := hello("")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(msg)
}
Try it: https://go.dev/play/p/XLfigSRITy7
Use fmt.Errorf() for formatted messages:
package main
import (
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Math error:", err)
return
}
fmt.Println("Result:", result)
}
Try it: https://go.dev/play/p/zN3jVXG8dq8
Good error messages:
Under the hood: errors.New() allocates a small struct containing the error message. The struct implements Error() string, satisfying the error interface. fmt.Errorf() formats the message first (like fmt.Sprintf()), then creates the error. These allocations are cheap but non-zero. For hot paths with frequent errors, pre-allocate error variables: var ErrNotFound = errors.New("not found") and return them. This avoids allocation. Go 1.13+ added error wrapping with fmt.Errorf("context: %w", err), preserving the original error while adding context.
Check and propagate:
if err != nil {
return err
}
Check and log:
if err != nil {
log.Println("Warning:", err)
// continue anyway
}
Check and exit:
if err != nil {
log.Fatal(err)
}
Check with fallback:
config, err := loadConfig()
if err != nil {
config = getDefaultConfig()
}
Check and retry:
for attempt := 0; attempt < 3; attempt++ {
err := tryConnect()
if err == nil {
break
}
time.Sleep(time.Second)
}
Under the hood: These patterns represent different error handling strategies. Propagation lets the caller decide what to do. Logging records errors but continues (useful for non-critical failures). Fatal exits are appropriate for initialization errors where the program cannot continue. Fallbacks enable graceful degradation. Retries handle transient failures (network timeouts, temporary unavailability). Each pattern is explicit. There’s no hidden exception mechanism that catches errors at unexpected locations.
How to inspect your program’s values while it runs.
Before you build real programs, you need to see what your code is doing. The fmt.Printf function has special formatting verbs that make debugging easier. This section teaches the three most useful ones.
The %v verb prints any value in a readable way:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Printf("%v\n", p)
}
Output:
{Alice 30}
Try it: https://go.dev/play/p/H1fGcqGI_8F
Use %v when you want quick output without formatting details.
The %+v verb shows field names for structs. This is the best everyday debugging format:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Printf("%+v\n", p)
}
Output:
{Name:Alice Age:30}
Try it: https://go.dev/play/p/rmb_kqkJYV9
Now you see exactly which values map to which fields.
The %#v verb prints values in Go literal format. This is the most detailed debugging output:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Printf("%#v\n", p)
}
Output:
main.Person{Name:"Alice", Age:30}
Try it: https://go.dev/play/p/8zTqJJ9Etsv
It includes the package path and shows the value as valid Go code.
Complex values benefit from the different formatting verbs:
package main
import "fmt"
type Address struct {
Street string
City string
}
type Person struct {
Name string
Age int
Address Address
}
func main() {
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Boston",
},
}
fmt.Printf("%%v: %v\n", p)
fmt.Printf("%%+v: %+v\n", p)
fmt.Printf("%%#v: %#v\n", p)
}
Output:
%v: {Alice 30 {123 Main St Boston}}
%+v: {Name:Alice Age:30 Address:{Street:123 Main St City:Boston}}
%#v: main.Person{Name:"Alice", Age:30, Address:main.Address{Street:"123 Main St", City:"Boston"}}
Try it: https://go.dev/play/p/Q8mD3dZRd3x
Notice how each verb provides more detail.
Under the hood: For nested structures, the formatter recursively applies the same formatting rules at each level. With %v, nested structs print without field names at any level. With %+v, all struct levels show field names. With %#v, the full type path appears for every level. The reflection system walks the nested structure, following pointers and embedded types. Deep nesting can make output verbose, but it shows the complete picture. The formatter handles cycles (a struct pointing to itself) by detecting repeated pointers and printing <cycle> to prevent infinite recursion.
| Verb | Use case | Example output |
|---|---|---|
%v | Quick print of any value | {Alice 30} |
%+v | Best for inspecting structs | {Name:Alice Age:30} |
%#v | Deep debugging; Go syntax | main.Person{Name:"Alice", Age:30} |
Guidelines:
%+v for struct debugging%#v when you need full type information%v for quick checks on simple valuesMix debugging verbs with other format specifiers:
package main
import "fmt"
type User struct {
ID int
Name string
}
func main() {
u := User{ID: 42, Name: "Bob"}
fmt.Printf("User %d: %+v\n", u.ID, u)
fmt.Printf("Type: %T, Value: %#v\n", u, u)
}
Output:
User 42: {ID:42 Name:Bob}
Type: main.User, Value: main.User{ID:42, Name:"Bob"}
Try it: https://go.dev/play/p/Kh5mT9D7JjX
The %T verb shows the type, which pairs well with %#v for complete debugging info.
How Go organizes code across files and folders.
Before you build anything bigger than a single file, you need to understand how Go organizes code. Go uses a folder-based structure. It’s simple but strict. This section gives you the foundation for multi-file projects.
A package in Go is simply a folder containing one or more .go files that all use the same package name.
Example folder structure:
mathutils/
add.go
subtract.go
Inside add.go:
package mathutils
func Add(a, b int) int {
return a + b
}
Inside subtract.go:
package mathutils
func Subtract(a, b int) int {
return a - b
}
Both files use package mathutils. They’re part of the same package.
Key rules:
.go files in the same folder must use the same package nameUnder the hood: The compiler treats all files in a package as one compilation unit. When you build a package, Go compiles all its .go files together. Functions, types, and variables are shared across files in the same package. Visibility rules (uppercase = exported, lowercase = unexported) apply at the package level, not the file level. This means files in the same package can access each other’s private functions. The package name becomes a namespace. Two packages can have functions with the same name without conflict because they’re in different namespaces.
Go has two kinds of packages.
Executable packages (use package main):
package main
func main() {
fmt.Println("This runs")
}
Rules:
package mainmain() functionLibrary packages (use descriptive names):
package mathutils
func Add(a, b int) int {
return a + b
}
Rules:
Under the hood: When you run go build, the toolchain looks for package main and a main() function. This signals an executable. The linker creates a binary with an entry point at main(). Without package main, the build tool produces a package archive (.a file) instead of an executable. Package archives contain compiled object code that other packages can link against. The distinction is fundamental. You cannot go run a library package. You cannot import package main into other packages (it’s treated specially by the toolchain).
To use another package, you import it.
Standard library package:
import "fmt"
Your own package:
import "learn-go/mathutils"
Multiple imports:
import (
"fmt"
"learn-go/mathutils"
)
Using imported packages:
package main
import (
"fmt"
"learn-go/mathutils"
)
func main() {
result := mathutils.Add(2, 3)
fmt.Println(result)
}
A module is the root of your project. It’s defined by a go.mod file.
Example go.mod:
module learn-go
go 1.22
The module path (learn-go) becomes the prefix for all your imports.
If you run:
go mod init learn-go
Then any folder under this module is imported like:
import "learn-go/examples/basics/hello"
Under the hood: The module system (introduced in Go 1.11, default since 1.16) replaced the old GOPATH approach. A module is a collection of packages with versioned dependencies. The go.mod file lives at your project root. It defines the module path (used as the import prefix) and the Go version. When you import third-party packages, Go adds require directives to go.mod automatically. The go.sum file (created automatically) contains checksums for dependencies, ensuring reproducible builds. The module path doesn’t need to match your folder name, but convention uses the repository path: github.com/user/project.
A minimal go.mod looks like this:
module learn-go
go 1.22
As you import third-party packages, Go updates it:
module learn-go
go 1.22
require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
)
You rarely edit this manually. The go command manages it.
What each part means:
module defines your import rootgo sets the minimum Go versionrequire lists dependencies and versionsUnder the hood: When you run go get github.com/user/package, the toolchain downloads the package, determines its version, and adds a require line to go.mod. The go mod tidy command removes unused dependencies and adds missing ones. Dependencies are downloaded to a shared cache ($GOPATH/pkg/mod or ~/go/pkg/mod). Multiple projects can share the same cached packages. The module system uses semantic versioning (v1.2.3) and supports version requirements (minimum version, exclude, replace). The go.sum file contains cryptographic checksums to detect tampering. This entire system ensures reproducible builds across machines.
Given this go.mod:
module learn-go
And this import:
import "learn-go/examples/basics/hello"
Go interprets it as:
examples/basics/helloThe mapping is:
learn-go/examples/basics/hello
↓
./examples/basics/hello
go run executes a folder containing package main:
go run .
This runs the main.go in the current folder.
For subfolders:
go run ./examples/basics/hello
You run folders, not individual files.
Under the hood: The go run command compiles the specified package to a temporary executable and runs it immediately. The binary is created in the system’s temp directory and deleted after execution. This is convenient for development but not suitable for production. For production, use go build to create a permanent binary. The go run . syntax means “run the package in the current directory.” The . is special syntax for the current folder. You can also specify files explicitly (go run main.go), but running by folder is more idiomatic because it handles multi-file packages correctly.