Go From Zero: A Beginner’s Guide to Golang Fundamentals — Part A
Introduction
This article is the first of a two-part series designed to give you a solid foundation in Go. This article covers everything from installation and basic syntax to pointers.
Prerequisites
A basic understanding of programming concepts, such as variables, loops, and pointers, is required to follow this article, as it focuses on Go-specific fundamentals. So prior experience with any programming language is sufficient. Prior knowledge of the C language is a bonus, since Go shares some syntactical similarities with it.
Ready to dive in? Let's get started…
What is Go?
Go is an open-source, general-purpose, strictly typed, and compiled programming language designed by Google. It was created by Robert Griesemer, Rob Pike, and Ken Thompson in 2007 and released in 2009. Sometimes, Go is referred to as “C-like language” or “C for the 21st century”. Actually, Go inherited C’s expression syntax, control-flow statements, basic data types, and pointers (Alan and Kernighan, 2015). The other major ancestor of Go is the Pascal language. However, unlike C, Go is a garbage-collected language.
Importantly, Go has rich standard libraries which are often described as "coming with batteries included", since they provide building blocks for APIs for I/O, text processing, graphics, cryptography, networking, and distributed applications.
Some use cases for Go are listed below.
- Command Line Interfaces (CLIs)
- Cloud and Network Services
- Web Development
- Development Operations and Site Reliability Engineering
Why is Go Popular?
Go’s popularity is currently defined by steady growth in its developer base, a rising position in global programming rankings, and a dominant role in cloud-native infrastructure. As of 2024–2025, estimates of the global Go developer community range from 4.7 million to 5.8 million, with approximately 1.8 million using it as their primary programming language (Ufliand, 2025; Rafalski, 2025).
Factors influenced Go’s popularity
- Simplicity
- Fast compile times
- Built-in concurrency (goroutines)
- Reduced cognitive load
Who Uses Go?
A wide variety of global organizations across the technology, finance, gaming, and infrastructure sectors utilize the Go programming language to handle massive scale and improve developer productivity.
- Google: As the creator of the language, Google uses Go across numerous engineering teams to build products and services at a massive scale.
- Microsoft: The company uses Go to power various pieces of its cloud infrastructure.
- Cloudflare: Go is central to Cloudflare’s services, including its DNS infrastructure, SSL, and load testing.
- Dropbox: Migrated performance-critical backends from Python to Go to achieve better concurrency and execution speed.
- Facebook: An engineering team at Facebook utilized Go to build a new entity framework (ORM)
- American Express: Uses Go for both its payment and rewards networks to ensure speed and scalability.
- PayPal: Leverages Go to modernize and scale its infrastructure, citing the language’s ability to produce clean, efficient code.
- Netflix: Uses Go for application data caching to achieve lower latency than Java while handling tens of thousands of connections.
- Riot Games: Uses Go to develop and operate backend microservices globally for its games.
Enough discussion, let’s get started….
Installation
Visit https://go.dev/doc/install and download the distribution suitable for your operating system. The official installation guide walks you through the setup process step by step. Once installed, open a terminal and run the following command to verify your installation:
go versionThis should output the installed Go version, confirming everything is set up correctly.
You can use any text editor of your choice. For this article, I’ll be using JetBrains GoLand as my IDE.
HelloWord
Create a file called hello-world.go and add the following code:
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}Then compile and run your file using the command below:
go run .\hello-world.goThis command compiles your Go file and immediately runs the resulting application. If everything is set up correctly, you should see the following output in your console:
Great! Now let's discuss these codes in the file.
package main— Every Go file must belong to a package.package mainis a special package that tells the Go compiler this file is the entry point of an executable program.func main()— This is the entry point of the application and must be defined insidepackage main. Whatever logic you place inside this function is what the program executes.import "fmt"— This imports Go's built-infmtlibrary, which provides functions for formatted I/O, such as printing to the console. We'll explore standard libraries in more detail later in the article.
Program Structure
Like most programming languages, Go programs are built from a small set of basic constructs: variables, expressions, operators, control-flow statements, and more. Related statements are grouped into functions, and functions are organized into packages.
Names
Names of the Go functions, variables, types, statements, labels, constants, and packages follow the simple rule below:
“A name begins with a letter or an underscore (_) and may have a number of additional letters, digits, and underscores.”
Importantly, names are case sensitive. However, there are 25 reserved keywords that can't be used as names, and 36 predeclared names for built-in constants, types, and functions. These predeclared names are not reserved. Figure 2 below shows the reserved keywords, and Figure 3 shows the predeclared names.
Variable Names and Their Visibility
- If a variable is declared within a function, it is local to that function. If it is declared outside of a function, it is visible to all files under the same package.
- The visibility of a name across package boundaries is determined by the case of its first letter; names starting with an upper-case letter are exported (accessible outside the package), while lower-case names are not.
- There is no limitation on the variable name length. However, convention and Go style prefer “cameCase” (e.g.,
parseRequestLine) and short names for local variables with small scopes.
Declarations
There are four major types of declarations: var (variables), const (constants), type (types), and func (functions).
- Package-level declarations are defined outside of any function and are visible across all files in that package.
- Local declarations are defined within a function and are visible only within that specific lexical block.
Variables and Pointers
Variables are created with a specific type and an initial value.
- Zero Values: If an initial value is not provided, Go implicitly initializes the variable to its “zero value” (0 for numbers,
falsefor booleans,""for strings, andnilfor interfaces and reference types). - Short Variable Declarations: Inside functions, the
:=Syntax can be used to declare and initialize local variables compactly, with the type inferred from the initializer. - Pointers: A pointer value is the address of a variable. The
&operator yields the address of a variable, and the*operator retrieves the value the pointer refers to. (Will discuss more about pointers under the Pointers sub-section) - Lifetime: Package-level variables live for the entire execution of the program, while local variables have dynamic lifetimes, existing until they become unreachable and are reclaimed by the garbage collector.
The code snippet below shows examples of variable declarations.
package main
import "fmt"
func main() {
// 1. explicit type + value
var firstName string = "John"
// 2. type inferred from value (no explicit type)
var lastName = "Doe"
// 3. zero value (type declared, no value)
var fullName string // zero value: ""
// 4. short declaration (inside functions only)
email := "abc@dec.lk"
// 5. multiple variables in one line
var x, y int = 1, 2
// 6. multiple short declarations in one line
a, b := 10, "hello"
// 7. block declaration (var block)
var (
city string = "Colombo"
country string = "Sri Lanka"
zipCode int = 10100
)
// 8. multiple assignment (swap)
x, y = y, x
// integer
var age int = 29
// sized integers
var eightBit int8 = 40
var sixtyFourBit int64 = -500
// unsigned integer
var unsignedInt uint = 190
// float
var score float32 = 89.5
// boolean
var isActive bool = true // explicit
var isVerified bool // zero value: false
isAdmin := false // short declaration
// constants (cannot use := )
const Pi float64 = 3.14159
const AppName = "MyGoApp" // type inferred
fmt.Println(firstName, lastName, fullName, email, x, y, a, b, city, country, zipCode,
age, eightBit, sixtyFourBit, unsignedInt, score, isActive, isVerified, isAdmin, Pi, AppName)
}Packages and Files
Packages serve the same purposes as libraries or modules in other languages, supporting modularity, encapsulation, and reuse.
- Imports: Every package is identified by a unique import path string. You must import exactly the packages you need; missing or unused imports result in a compilation error.
package main
// importing a single package
import "fmt"
// importing multiple packages
import (
"fmt"
"math"
)- Initialization: Package initialization begins by resolving dependencies and then initializing package-level variables in the order they are declared. Go also supports init functions, which are automatically executed when the program starts.
Scope
Scope refers to the region of the source code in which a name is valid. The scope of a declaration is a region of the program text, and it is a compile-time property.
Lexical Blocks
Go uses lexical blocks to define scope. These include:
- Universe block — covers built-in types (
int,string,bool) and built-in functions (len,cap,make) that are available everywhere in your program. - Package block — variables and functions declared outside of any function are accessible throughout the entire package.
- File block — applies to
importdeclarations, which are only visible within the file they are declared in. - Local blocks — variables declared inside functions,
if,for, orswitchstatements are only accessible within that block.
package main
import "fmt"
// package block — accessible throughout the package
var appName string = "MyGoApp"
func main() {
// local block — only accessible within main()
var message string = "Hello from main"
fmt.Println(appName, message)
if true {
// inner local block — only accessible within this if block
var innerMessage string = "Hello from if block"
fmt.Println(innerMessage)
}
// fmt.Println(innerMessage) // ERROR: innerMessage is not accessible here
}Shadowing
When a variable in an inner block shares the same name as a variable in an outer block, the inner declaration shadows (hides) the outer one within that block. The outer variable is not modified — it is simply inaccessible by that name inside the inner block.
package main
import "fmt"
var score int = 100 // package-level variable
func main() {
fmt.Println(score) // Output: 100 (package-level score)
score := 50 // shadows the package-level score inside main()
fmt.Println(score) // Output: 50 (local score)
if true {
score := 25 // shadows the local score inside the if block
fmt.Println(score) // output: 25
}
fmt.Println(score) // output: 50 (back to main()'s local score)
}Data Types
As discussed, Go is a strictly typed language. Go’s data types are organized into categories, ranging from those that match hardware features to those designed for the convenient representation of complex structures. There are two major categories of data types. Table 1 below shows the data types in each category.
Basic Data Types
Integers: Go provides both signed and unsigned integer arithmetic in four distinct sizes: 8, 16, 32, and 64 bits.
- Signed:
int8,int16,int32,int64. - Unsigned:
uint8,uint16,uint32,uint64. - Platform-dependent:
intanduintare the most efficient size for the specific platform (typically 32 or 64 bits). - Special synonyms:
runeis a synonym forint32(used for Unicode code points), andbyteis a synonym foruint8. - Low-level:
uintptris an unsigned integer wide enough to hold a pointer value.
Hands-on experience: Let's run the Go file 1 below and identify the different varieties of integer data types!
// Go file 1
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
// signed integers
var i8 int8 = 127 // range: -128 to 127
var i16 int16 = 32_767 // range: -32,768 to 32,767
var i32 int32 = 2_147_483_647 // range: -2^31 to 2^31-1
var i64 int64 = math.MaxInt64 // range: -2^63 to 2^63-1
fmt.Println("signed Integers")
fmt.Printf("int8 : %d (size: %d byte)\n", i8, unsafe.Sizeof(i8))
fmt.Printf("int16 : %d (size: %d bytes)\n", i16, unsafe.Sizeof(i16))
fmt.Printf("int32 : %d (size: %d bytes)\n", i32, unsafe.Sizeof(i32))
fmt.Printf("int64 : %d (size: %d bytes)\n", i64, unsafe.Sizeof(i64))
// unsigned integers
var u8 uint8 = 255 // range: 0 to 255
var u16 uint16 = 65_535 // range: 0 to 65,535
var u32 uint32 = 4_294_967_295 // range: 0 to 2^32-1
var u64 uint64 = math.MaxUint64 // range: 0 to 2^64-1
fmt.Println("\n unsigned Integers ")
fmt.Printf("uint8 : %d (size: %d byte)\n", u8, unsafe.Sizeof(u8))
fmt.Printf("uint16 : %d (size: %d bytes)\n", u16, unsafe.Sizeof(u16))
fmt.Printf("uint32 : %d (size: %d bytes)\n", u32, unsafe.Sizeof(u32))
fmt.Printf("uint64 : %d (size: %d bytes)\n", u64, unsafe.Sizeof(u64))
// platform-dependent
var i int = math.MaxInt
var u uint = math.MaxUint
fmt.Println("\n platform-Dependent ")
fmt.Printf("int : %d (size: %d bytes)\n", i, unsafe.Sizeof(i))
fmt.Printf("uint : %d (size: %d bytes)\n", u, unsafe.Sizeof(u))
// Special synonyms
// byte is an alias for uint8 — typically used for raw binary data
var b byte = 'A'
fmt.Println("\n special synonyms ")
fmt.Printf("byte : %d '%c' (alias for uint8, size: %d byte)\n", b, b, unsafe.Sizeof(b))
// rune is an alias for int32 — used for Unicode code points
var r rune = '🎉'
fmt.Printf("rune : %d '%c' (alias for int32, size: %d bytes)\n", r, r, unsafe.Sizeof(r))
fmt.Println()
// uintptr
x := 42
ptr := uintptr(unsafe.Pointer(&x)) // convert pointer to uintptr for arithmetic
fmt.Println("\n low-Level: uintptr ")
fmt.Printf("uintptr: 0x%X (size: %d bytes, holds pointer to x=%d)\n",
ptr, unsafe.Sizeof(ptr), x)
// overflow demonstration
var overflow uint8 = 255
overflow++ // wraps around to 0
var underflow int8 = -128
underflow-- // wraps around to 127
fmt.Println("\noverflow / underflow (wrap-around)")
fmt.Printf("uint8(255)++ = %d (wrapped to 0)\n", overflow)
fmt.Printf("int8(-128)-- = %d (wrapped to 127)\n", underflow)
}Floating-Point Numbers: There are two sizes, float32 and float64, which follow the IEEE 754 standard. float64 is preferred for most purposes because float32 accumulates error rapidly.
Complex Numbers: Go provides complex64 andcomplex128, which uses float32 and float64 components respectively. They are created using the built-in complex function.
Hands-on experience: Let’s run the Go file 2 below to check on float and complex numbers data types.
// Go file 2
package main
import (
"fmt"
"math"
"math/cmplx"
)
func main() {
// floating-point
// float32 and float64 both store the same literal,
// but float32 loses precision quickly.
var f32 float32 = 3.141592653589793
var f64 float64 = 3.141592653589793
fmt.Println("Floating-Point ")
fmt.Printf("float32: %.10f (precision lost)\n", f32)
fmt.Printf("float64: %.15f (preferred)\n", f64)
fmt.Printf("Sqrt(2): %.15f\n", math.Sqrt(2))
// complex numbers
// complex64 uses float32 components
// complex128 uses float64 components (preferred)
var c64 complex64 = complex(1, 2) // 1 + 2i
var c128 complex128 = complex(3, 4) // 3 + 4i
fmt.Println("\nComplex Numbers ")
fmt.Printf("complex64 : %v real=%.1f imag=%.1f\n", c64, real(c64), imag(c64))
fmt.Printf("complex128: %v real=%.1f imag=%.1f\n", c128, real(c128), imag(c128))
fmt.Printf("c128 * c128 : %v\n", c128*c128)
fmt.Printf("Abs(3+4i) : %.1f\n", cmplx.Abs(c128))
fmt.Printf("Euler: %.0f\n", real(cmplx.Exp(complex(0, math.Pi))+1)) // ≈ 0
}Booleans: The bool type has only two possible values: true and false. They are often used in control-flow statements like if and for.
Strings: A string is an immutable sequence of bytes. While they can contain arbitrary data, they are conventionally interpreted as UTF-8-encoded Unicode text.
Composite Data Types
Composite types are the “molecules” created by combining basic types. Among the four composite data types, we will discuss Arrays and Slices in this article. The remaining will be discussed in Part B of this article.
Arrays: An array is a fixed-length sequence of zero or more elements of a particular type. Because the size is fixed, the length is considered part of the array’s type. They are rarely used directly in Go in favor of slices.
Slices: Slices represent variable-length sequences where all elements have the same type. A slice is a lightweight structure consisting of three components:
- A pointer: Points to the first reachable element of an underlying array.
- Length: The number of elements currently in the slice.
- Capacity: Typically, the number of elements between the start of the slice and the end of the underlying array. Unlike arrays, slices are dynamic and can grow using the built-in
appendfunction.
Hands-on experience: Let’s run the Go file 3 below to learn more about Arrays and Slices.
// Go file 3
package main
import "fmt"
func main() {
// arrays - fixed size, all elements must be the same type
var numbersArr [3]int = [3]int{1, 2, 3}
var names = [3]string{"John", "Amy", "Peter"}
fmt.Println(numbersArr, len(numbersArr))
fmt.Println(names, len(names))
// accessing and updating elements by index
fmt.Println("first name:", names[0])
names[0] = "Alice"
fmt.Println("after update:", names)
// iterating over an array with range
for i, v := range numbersArr {
fmt.Printf("index %d -> %d\n", i, v)
}
// slices - backed by an array, but size is flexible
var scores = []int{50, 69, 20}
fmt.Println(scores, len(scores), cap(scores)) // cap shows underlying array capacity
// append adds to the end and returns a new slice
// if capacity is exceeded, go allocates a larger backing array
scores = append(scores, 59)
fmt.Println(scores, len(scores), cap(scores))
// appending multiple elements at once
scores = append(scores, 100, 200)
fmt.Println(scores, len(scores), cap(scores))
// merging two slices with ...
extra := []int{1, 2, 3}
scores = append(scores, extra...)
fmt.Println(scores, len(scores))
// slice ranges - [low : high] where low is inclusive and high is exclusive
rangeOne := names[1:3] // index 1 up to (not including) 3
fmt.Println(rangeOne, len(rangeOne))
rangeTwo := names[:2] // from the beginning up to index 2
fmt.Println(rangeTwo, len(rangeTwo))
rangeThree := names[2:] // from index 2 to the end
fmt.Println(rangeThree, len(rangeThree))
// ranges share the backing array, so edits affect the original
rangeTwo[0] = "Bob"
fmt.Println("names after editing rangeTwo:", names)
// use copy to get an independent slice that does not affect the original
nameCopy := make([]string, len(names))
copy(nameCopy, names[:])
nameCopy[0] = "Charlie"
fmt.Println("original names:", names)
fmt.Println("independent copy:", nameCopy)
// make - create a slice with a given length and optional capacity
blank := make([]int, 3, 5) // length 3, capacity 5
fmt.Println(blank, len(blank), cap(blank))
}Standard Library
The Go standard library, often described as having “batteries included,” contains over 100 packages that provide essential building blocks for common programming tasks. These packages cover a wide range of functionality, including input/output (I/O), text processing, graphics, cryptography, networking, and distributed applications. By relying on these standard packages, developers can write clear, idiomatic, and efficient programs while minimizing the need for external configurations or complex third-party frameworks.
Get Lakeesha Ramanayake’s stories in your inbox
Join Medium for free to get updates from this writer.
Here we discuss some important standard packages.
The fmt Package
The fmt package is one of the most frequently used components of the standard library, primarily used for formatting output and scanning input. It facilitates the display of data in a human-readable format and is central to basic debugging and user interaction.
Core Functions: The package provides several key functions for different output destinations. fmt.Println prints values separated by spaces with a newline at the end. fmt.Printf produces formatted output based on a "format string" containing conversion characters, which Go programmers call verbs. fmt.Fprintf is a more general version that writes to any object satisfying the io.Writer interface, such as a file or a network connection. fmt.Sprintf returns the formatted result as a string instead of printing it.
Common Verbs: Formatting verbs allows precise control over data representation. Some common ones include:
- %v: Formats any value in its natural default format.
- %T: Displays the type of any value.
- %d: Formats a decimal integer.
- %s: Formats a string or a byte slice interpreted as text.
- %q: Provides a quoted string (e.g.,
"abc"). - %f, %g, %e: Various formats for floating-point numbers.
Hands-on experience: Let’s run the Go file 4 below to interact with the fmtpackage.
// Go file 4
package main
import (
"fmt"
"os"
)
func main() {
name := "John Doe"
age := 29
pi := 3.142857142857143
// fmt.Println - values separated by spaces, newline at the end
fmt.Println("name:", name, "age:", age)
// fmt.Printf - formatted output using verbs
fmt.Printf("%v is %v years old\n", name, age) // %v - default format
fmt.Printf("name: %q\n", name) // %q - quoted string
fmt.Printf("age: %d\n", age) // %d - decimal integer
fmt.Printf("type of age: %T\n", age) // %T - type of value
fmt.Printf("pi (2 decimal): %.2f\n", pi) // %f - float, precision specified
fmt.Printf("pi (scientific): %e\n", pi) // %e - scientific notation
fmt.Printf("pi (compact): %g\n", pi) // %g - whichever is shorter, %e or %f
// fmt.Sprintf - returns formatted string instead of printing
summary := fmt.Sprintf("%q is %d years old and pi is %.4f", name, age, pi)
fmt.Println(summary)
// fmt.Fprintf - writes to any io.Writer, here using os.Stdout and os.Stderr
fmt.Fprintf(os.Stdout, "writing %q to stdout\n", name)
fmt.Fprintf(os.Stderr, "writing %q to stderr\n", name)
}The math Package
The math package provides a comprehensive collection of standard mathematical functions and constants for numerical computation.
- Constants: It defines important mathematical constants such as
math.Piand limit values for different numeric types, such asmath.MaxFloat64. - Functions: The package includes standard functions like
math.Sin,math.Cos,math.Exp,math.Sqrt, andmath.Hypot. - Special Values: It contains functions for creating and detecting special IEEE 754 floating-point values, such as positive/negative infinities and NaN (Not-a-Number). For instance,
math.IsNaNcan be used to check if a result is numerically undefined. - Specialized Math Packages: Beyond the basic
mathpackage, the standard library includes math/cmplx for complex-number arithmetic and math/big for arbitrary-precision arithmetic.
Hands-on experience: Let’s run the Go file 5 below to interact with the mathpackage.
// Go file 5
package main
import (
"fmt"
"math"
"math/big"
"math/cmplx"
)
func main() {
// constants
fmt.Printf("math.Pi: %.15f\n", math.Pi)
fmt.Printf("math.E: %.15f\n", math.E)
fmt.Printf("math.MaxFloat64: %g\n", math.MaxFloat64)
fmt.Printf("math.MaxInt64: %d\n", math.MaxInt64)
// common functions
fmt.Printf("Sqrt(16): %g\n", math.Sqrt(16))
fmt.Printf("Pow(2, 8): %g\n", math.Pow(2, 8))
fmt.Printf("Abs(-7.5): %g\n", math.Abs(-7.5))
fmt.Printf("Hypot(3, 4): %g\n", math.Hypot(3, 4))
// trigonometry (arguments in radians)
fmt.Printf("Sin(90): %g\n", math.Sin(90))
fmt.Printf("Cos(90): %g\n", math.Cos(90))
// exponential and logarithm
fmt.Printf("Exp(1): %g\n", math.Exp(1)) // e^1
fmt.Printf("Log(E): %g\n", math.Log(math.E)) // natural log
fmt.Printf("Log10(100): %g\n", math.Log10(100))
// special ieee 754 values
inf := math.Inf(1)
nan := math.NaN()
fmt.Printf("Inf: %v\n", inf)
fmt.Printf("NaN: %v\n", nan)
fmt.Printf("IsInf: %v\n", math.IsInf(inf, 1))
fmt.Printf("IsNaN: %v\n", math.IsNaN(nan))
fmt.Printf("1 / Inf: %v\n", 1/inf) // approaches 0
fmt.Printf("Inf + 1: %v\n", inf+1) // still Inf
// math/cmplx - complex number arithmetic
c := complex(3.0, 4.0) // 3 + 4i
fmt.Printf("cmplx.Abs(3+4i): %g\n", cmplx.Abs(c)) // magnitude = 5
fmt.Printf("cmplx.Sqrt(-1): %v\n", cmplx.Sqrt(-1)) // 0+1i
// math/big - arbitrary precision (no float64 size limit)
x := new(big.Int).Exp(big.NewInt(2), big.NewInt(100), nil) // 2^100
fmt.Printf("2^100: %v\n", x)
}Other Popular Packages
os: Provides a platform-independent interface to operating system functionality, such as accessing command-line arguments viaos.Args, opening files withos.Open, and interacting with standard input/output streams.stringsandbytes: These packages offer similar sets of functions for searching, replacing, trimming, and splitting text data, optimized for strings and byte slices, respectively.strconv: Handles conversions between basic data types and their string representations, such as converting an integer to a string usingstrconv.Itoaor parsing a string into an integer withstrconv.Atoi.time: Used for measuring and displaying time, providing types such astime.Durationand functions liketime.Nowfor retrieving the current timestamp.net/http: A powerful package for building both HTTP clients (for fetching web data) and web servers. It uses thehttp.Handlerinterface as its core abstraction for responding to web requests.
Loops
Go has only one construct for the loops, which is for loop. It is designed to handle all types of iterations.
for Loop Skeleton
The standard form of a for loop consists of three optional components separated by semicolons:
for initialization; condition; post {
// zero or more statements
}- Initialization: Executed once before the loop starts. It is typically a short variable declaration, an assignment, or a function call.
- Condition: A boolean expression evaluated at the start of each iteration. If
true, the loop body executes; iffalse, the loop terminates. - Post: Executed after the loop body on every iteration, then the condition is re-evaluated.
Syntax Rules:
- Parentheses are never used around the three components.
- Braces
{}are mandatory, and the opening brace must be on the same line as the post statement.
Variants of the for Loop
1. Traditional “While” Loop
By omitting the initialization and post statements, you create a loop that functions like a “while” loop in other languages.
// a traditional "while" loop
for condition {
// ...
}2. Infinite Loop
If the condition is also omitted, the loop becomes infinite
// a traditional "infinite" loop
for {
// ...
}Common Usages of the for Loop
1. Iterating with range
This form is used to iterate over elements in a data structure like a string, slice, array, or map. In each iteration, range produces a pair of values: the index (or key) and the element value.
for index, value := range collection {
// use index and value
}If you only need the value and not the index, you can use the blank identifier (_) to discard the unwanted index.
2. UTF-8 Decoding on Strings
When applied to a string, the range loop automatically performs UTF-8 decoding, allowing you to iterate over individual Unicode code points (runes) rather than just raw bytes.
Loop Control Statements
You can modify the flow of a for loop using two key statements:
- break: Causes the loop to terminate immediately and resumes execution at the next statement after the loop.
- continue: Skips the rest of the current iteration and starts the next one.
Both break and continue can be used with labels to jump out of multiple nested loops at once.
Hands-on experience: Use Go file 6 below to try out the Go’s looping construct.
// Go file 6
package main
import "fmt"
func main() {
// standard for loop: initialization; condition; post
for i := 0; i < 5; i++ {
fmt.Printf("i = %d\n", i)
}
fmt.Println()
// "while" style - omit initialization and post
x := 0
for x < 3 {
fmt.Printf("x = %d\n", x)
x++
}
fmt.Println()
// infinite loop with break
count := 0
for {
if count == 3 {
break
}
fmt.Printf("count = %d\n", count)
count++
}
fmt.Println()
// continue - skip even numbers
for i := 0; i < 6; i++ {
if i%2 == 0 {
continue
}
fmt.Printf("odd: %d\n", i)
}
fmt.Println()
// range over a slice - index and value
names := []string{"Alice", "Bob", "Charlie", "Diana"}
for index, value := range names {
fmt.Printf("names[%d] = %s\n", index, value)
}
fmt.Println()
// range - discard index with blank identifier
for _, name := range names {
fmt.Printf("name: %s\n", name)
}
fmt.Println()
// range over a string - decodes utf-8 runes automatically
for index, r := range "héllo" {
fmt.Printf("index %d: %c (rune %d)\n", index, r, r)
}
fmt.Println()
// labeled break - exit outer loop from inner loop
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i+j == 3 {
break outer
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
}Conditionals
Conditionals in Go, such as if-else and switch statements are fundamental control-flow mechanisms used to determine the execution path of a program based on logic.
if-else Statements
The if statement is used for conditional execution. In Go, parentheses are never used around the condition, but the braces {} are mandatory, and the opening brace must be on the same line as the condition.
if initialization; condition {
// ...
} else if condition {
// ...
} else {
// ...
}Initialization Statement: An if statement can include an optional simple statement (like a short variable declaration or function call) that precedes the condition. This is frequently used for error handling to limit the scope of the error variable.
switch Statements
A switch statement provides a multi-way branch, comparing the result of an operand against the values in various cases.
switch initialization; operand {
case value1:
// ...
case value2, value3:
// ...
default:
// ...
}Implicit Break: Unlike C-like languages, cases in Go do not “fall through” to the next one; execution stops after a matching case body is finished. A rarely used fallthrough statement can be used to override this.
Tagless Switch: A switch can be written without an operand, which is equivalent to switch true. In this form, each case must be a boolean expression.
Hands-on experience: Let’s use the Go file 7 below to try out a few conditional statements.
// Go file 7
package main
import "fmt"
func main() {
// basic if-else
age := 34
if age < 30 {
fmt.Println("under 30")
} else if age < 40 {
fmt.Println("in the 30s")
} else {
fmt.Println("40 or older")
}
fmt.Println()
// initialization statement in if
// score is scoped to the if-else block only
if score := 82; score >= 90 {
fmt.Println("grade: A")
} else if score >= 75 {
fmt.Println("grade: B")
} else {
fmt.Println("grade: C")
}
fmt.Println()
// standard switch - implicit break after each case
day := "Tuesday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println(day, "is a weekday")
case "Saturday", "Sunday":
fmt.Println(day, "is a weekend")
default:
fmt.Println("unknown day")
}
fmt.Println()
// fallthrough - explicitly continue into the next case
n := 1
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two (via fallthrough)")
case 3:
fmt.Println("three")
}
fmt.Println()
// tagless switch - each case is a boolean expression
temp := 72
switch {
case temp < 32:
fmt.Println("freezing")
case temp < 60:
fmt.Println("cold")
case temp < 80:
fmt.Println("comfortable")
default:
fmt.Println("hot")
}
fmt.Println()
// scope - variable declared in switch init is local to the block
switch result := age * 2; {
case result > 100:
fmt.Println("result is over 100:", result)
default:
fmt.Println("result is:", result)
}
// result is not accessible here
}Functions
Like other programming languages, functions allow us to organize the sequence of statements as a unit that can be called from elsewhere in the program. They enable breaking a large procedure into smaller pieces (procedure decomposition).
Function Declarations
func name(parameterlist) (resultlist){
body
}A standard function declaration includes the func keyword, the function name, a parameter list, an optional result list, and the body.
- Signatures: A function’s “signature” is defined by the sequence of its parameter types and result types.
- Parameters and Results: If multiple parameters or results share the same type, the type can be written just once (e.g.,
func f(i, j int)). Results can also be named, acting as local variables initialized to their zero values. - Call-by-Value: Go is a call-by-value language; functions receive a copy of each argument. However, if an argument is a reference type (like a pointer, slice, or map), the function can modify the variables indirectly referred to by that argument.
Additionally, functions can return multiple values (refer to the summary function in the Go file 8 below), a feature frequently used to return both a result and an error. In functions with named results, a “bare return” (a return statement without operands) can be used to return the current values of those named variables, though this is recommended only for short functions.
Hands-on experience: Go file 8 below contains a few example functions you can try out in your local environment.
// Go file 8
package main
import (
"fmt"
"math"
"math/rand"
"time"
)
func main() {
// basic call
greet("Alice")
// multiple return values
area, perimeter := circleStats(5.0)
fmt.Printf("area=%.2f perimeter=%.2f\n", area, perimeter)
// named results
min, max, avg := summary(randomSlice(10))
fmt.Printf("min=%.4f max=%.4f avg=%.4f\n", min, max, avg)
// functions as arguments
names := []string{"Alice", "Bob", "Charlie"}
applyToEach(names, greet)
applyToEach(names, farewell)
}
// greet and farewell share the same signature: func(string)
func greet(name string) {
fmt.Printf("hello, %s!\n", name)
}
func farewell(name string) {
fmt.Printf("goodbye, %s!\n", name)
}
// applyToEach takes a function as an argument and applies it to each element
func applyToEach(names []string, fn func(string)) {
for _, name := range names {
fn(name)
}
}
// circleStats returns multiple values - shared type written once for the result list
func circleStats(radius float64) (float64, float64) {
return math.Pi * math.Pow(radius, 2), 2 * math.Pi * radius
}
// summary uses named results - they act as local variables initialised to zero
// the bare return at the end returns their current values
func summary(numbers []float64) (min, max, avg float64) {
if len(numbers) == 0 {
return
}
min, max = numbers[0], numbers[0]
var sum float64
for _, v := range numbers {
if v < min {
min = v
}
if v > max {
max = v
}
sum += v
}
avg = sum / float64(len(numbers))
return // bare return
}
// randomSlice generates a slice of n random float64 values
func randomSlice(n int) []float64 {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
s := make([]float64, n)
for i := range s {
s[i] = rng.Float64()
}
return s
}Function Values and Anonymous Functions
Functions are first-class values in Go, meaning they can be assigned to variables or passed as arguments to other functions.
- Anonymous Functions: These are defined at their point of use without a name.
- Closures: Anonymous functions can access and update variables from their enclosing lexical environment, a technique called “closures”.
- Pitfall: A common mistake is capturing iteration variables in a loop; because closures capture the variable itself rather than its value at a specific moment, all closures in a loop may end up referring to the value of the final iteration.
Hands-on experience: Use Go file 9 below to interact with anonymous functions.
package main
import "fmt"
func main() {
// function assigned to a variable
double := func(n int) int { return n * 2 }
fmt.Println(double(5))
// immediately invoked anonymous function
fmt.Println(func(a, b int) int { return a + b }(3, 4))
// closure - counter owns its count variable across calls
counter := makeCounter()
fmt.Println(counter(), counter(), counter()) // 1 2 3
// pitfall - capturing iteration variables in a loop
functions := make([]func(), 3)
i := 0
for ; i < 3; i++ {
functions[i] = func() { fmt.Print("pitfall: ", i, " ") }
}
for _, f := range functions {
f() // prints 3 3 3
}
fmt.Println()
// fix - capture the value explicitly at each iteration
for j := 0; j < 3; j++ {
j := j
functions[j] = func() { fmt.Print("fixed: ", j, " ") }
}
for _, f := range functions {
f() // prints 0 1 2
}
fmt.Println()
}
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
Pointers
A pointer contains the address of a variable. In other words, a pointer is pointing to a variable. Not every value has an address, but every variable has one. Using a pointer, we can update or read the value of a variable without using its name, but using its memory address. However, pointer arithmetic is not available in Go.
The pointer data type should match the type of the variable being referenced. If we want to store an address of an integer variable, then the pointer variable must be an integer pointer. We declare the pointer variable as *int type, which is pronounced as “pointer to int”. This theory is applied to all data types.
- Address-of Operator (&): When applied to a variable, the
&operator yields its memory address. For example, if a variable is declared asvar x int, then&xyields a value of type*int(pronounced "pointer to int"). - Indirection Operator (*): This operator is used to retrieve the value the pointer refers to, an action often called dereferencing. If
pis a pointer tox, then*pyields the value ofx. - Assignment: Because
*pdenotes a variable, it can also appear on the left-hand side of an assignment to update the value of the variable the pointer refers to (e.g.,*p = 2sets the variablexto 2)
Key Properties
- Zero Value: The zero value for a pointer of any type is nil. A test like
p != nildetermines if a pointer actually refers to a variable. - Comparability: Two pointers are considered equal if they point to the same variable or if both are
nil. - Safety of Local Addresses: Unlike some languages, it is perfectly safe for a function to return the address of a local variable. The variable will continue to exist even after the function call has returned as long as the pointer still refers to it.
- Function Arguments: Passing a pointer to a function allows that function to update the original variable indirectly.
Hands-on experience: Use the Go file 10 below to try out the pointers.
// Go file 10
package main
import "fmt"
func main() {
// & yields the memory address of a variable
x := 42
p := &x
fmt.Printf("x = %d, address = %v\n", x, p)
// * dereferences the pointer - reads the value at the address
fmt.Printf("*p = %d\n", *p)
// * on the left side updates the variable through the pointer
*p = 100
fmt.Printf("x after *p = 100: %d\n", x)
// zero value of a pointer is nil
var ptr *int
fmt.Printf("uninitialized pointer: %v, is nil: %v\n", ptr, ptr == nil)
// two pointers are equal if they point to the same variable
p1, p2 := &x, &x
fmt.Printf("p1 == p2 (same variable): %v\n", p1 == p2)
// safe to return address of a local variable (see newInt below)
managed := newInt(99)
fmt.Printf("value from returned local pointer: %d\n", *managed)
// pass by value - function gets a copy, original is unchanged
name := "Alice"
passByValue(name)
fmt.Printf("after passByValue: %s (unchanged)\n", name)
// pass by pointer - function updates the original variable
passByPointer(&name)
fmt.Printf("after passByPointer: %s (updated)\n", name)
}
// newInt returns the address of a local variable - safe in go
func newInt(v int) *int {
n := v
return &n
}
// passByValue receives a copy - changes do not affect the caller
func passByValue(s string) {
s = "Bob"
fmt.Printf("inside passByValue: %s\n", s)
}
// passByPointer receives the address - changes affect the original variable
func passByPointer(s *string) {
*s = "Bob"
fmt.Printf("inside passByPointer: %s\n", *s)
}Part A ends here. This article covered the fundamental entities of the Go language. Part B will extend this article by discussing maps, structs, receiver functions, and related topics in more detail.
Until next time!
Artificial Intelligence (AI) Usage Declaration
The authors acknowledge the use of Claude 4.6 Sonnet and Grammarly AI to improve language clarity, assist with grammar checking, and help organize the article example Go files.
The prompts used are “Generate example Go file to explain basic data types, floating point and complex numbers, arrays and slices”, “Generate example Go file to show the usages of fmt, math libraries”, and “Generate an example Go file to cover the usages of Pointers”. The output from these prompts was used to organize the example Go files used in this article.
While the author acknowledges the usage of AI, the authors maintain that they, A.R.L.P.B Ramanayake, are the sole authors of this article and take full responsibility for the content therein.
References
Alan and Kernighan, B.W. (2015). The Go Programming Language. Addison-Wesley Professional.
Kulesza, T. (2026) Results from the 2025 Go Developer Survey. [online] Go.dev. Available at: https://go.dev/blog/survey2025-results [Accessed 9 May 2026].
Rafalski, K. (2025) Why Golang’s Popularity Is Soaring: Real Data From Top Tech Companies. [online] Netguru Blog. Available at: https://www.netguru.com/blog/why-golang-popularity-is-soaring [Accessed 9 May 2026].
TIOBE (2026) TIOBE Index for May 2026. [online] TIOBE. Available at: https://www.tiobe.com/tiobe-index/ [Accessed 9 May 2026].
Ufliand, E. (2025) Is Golang Still Growing? Go Language Popularity Trends in 2024. [online] JetBrains Research. Available at: https://blog.jetbrains.com/research/2025/04/is-golang-still-growing-2024-trends/ [Accessed 9 May 2026]
The Go Programming Language (n.d.) Case Studies — The Go Programming Language. [online] Available at: https://go.dev/solutions/case-studies [Accessed 9 May 2026].








