This repository is for my CS-330 class at Simmons University where my goal is to create a simple straightfoward tutorial for getting started using Go for students already comfortable with at least one programming language. This is a work-in-progress so there will be some errors, omissions and incomplete sections. When the project is complete and the semester is over, I will tag it complete and the repo will allow pull requests. I'm currently working in Windows 11, and while I try to give instructions for Mac and Linux, I can't test them all on my machine and so I'm mainly going by documentation sources. Your miles may vary. Until then, if you have any issues or commentary, please see the Discussion section of the repository.
If you need to reach me, message me on LinkedIn
Credits:
Side by Side Markdown Editor: Dillinger.io
History of the Go Programming Language
Important Features of Go
What is Go good for?
Famous Projects Created with Go
Installation and Setup
Installing Go
Choosing an IDE
Installing VSCode Go Extension
Creating Your First Go Project in VSCode
Hello World!
Commenting Your Code
Go Learning Resources
Type System and Variable Semantics
Data Types
Basic Types
Boolean
Numeric
Strings
Aliases
Composite Types
Aggregate: Arrays, Structs
Reference: Slices, Maps, Channel, Pointer
Interface Types
Complex Types
Type Conversion
Syntax
Reserved Words
Variable Naming Requirements & Conventions
Required by Compiler
Encouraged by Professional and Community Standards
Composite Literals
Type Aliases vs. Defined Types
Operators
Availability
Binding
Storage, Addresses and Lifetime
Scope
Limitations
Pitfalls
Control Statements: Loops, Selection, Conditionals
If/Else
For Loops
Continue and Break
Labels
Switch Statements
Block Delimiting
Short Circuit Evaluation
Why is Go Missing Things?
Function Declaration & Syntax
Function Scope
Function Passby
Side Effects
Guardrails
Memory Management
Recursive Functions
Objects in Go
Naming Conventions
Standard Methods
Stringer
Error
Reader/Writer
Functions vs Methods
Mock Inheritance
Mock Multiple Inheritance
Overloading Method Names
Value vs Pointer Receivers
Other Considerations
The Go programming language, often referred to as Golang, was created at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson as a direct response to the frustrations experienced in software development within the company.1 The main catalyst for its creation was the difficult nature of using existing languages for massive systems work, lengthy compilation times for languages like C and the complexity of distributed systems built in languages like Java.2 The designers were also motivated by a desire to improve the scale of development for large teams of programmers working on shared codebases.3 Officially announced in 2009 as an open-source project, Go was designed to be a simple language that emphasized ease of use for modern hardware but with the power of other languages already in use at the company.4
If you want an interesting read about the beginnings of Go, check out the official Go Spec Blob on Github by Robert Griesemer himself.
- Simplicity and Minimalism
There are no classes or inheritance5, exceptions6, or higher order functions7 in Go. These are achieved with workarounds. - Built-in Concurrency Primitives: Goroutines and Channels8
- Fast Compilation to a Single, Static Binary9
- Explicit Error Handling10
- Composition over Inheritance11
- Built-in Tooling12
- Garbage Collection13
- Generics (Recent Addition)14
A full tour of Go by Russ Cox is available on YouTube here.
- Cloud-Native & Distributed Systems (Microservices)15
- Command-Line Interfaces (CLIs)16 & DevOps Tools17
- Web Servers & API Backends18
- Concurrent Network Services19
- Data Processing & Pipelines20
- Databases & Storage Systems21
- Cryptography22 & Security Tools23
- Embedded Systems24 & IoT25
- Scripting26 & Automation27
- Proxy28 and Load Balancer Infrastructure29
- Docker A platform to develop, ship, and run applications in containers.
- Kubernetes An open-source system for automating deployment of containerized applications.
- Hugo A fast and modern static site generator.
- Terraform A tool for building, changing, and versioning infrastructure.
- CockroachDB A cloud-native, distributed SQL database.
- InfluxDB An open-source time series database.
- Ethereum Go (Geth) The official Go implementation of the Ethereum protocol.
- Caddy An open-source web server with automatic HTTPS.
- Syncthing A continuous real-time file synchronization program.
- Dgraph A distributed and transactional native GraphQL database.
The first step is to download and install Go. You can get the download here.
Windows
- Open the MSI file you downloaded and follow the prompts to install Go.
- You can change the location of your installation as needed. After installing, you will need to close and reopen any open command prompts.
- Verify that you've installed Go. In Windows, click the Start menu. In the menu's search box, type cmd, then press the Enter key.
- In the Command Prompt window that appears, type the following command:
> go version- Confirm that the command prints the installed version of Go.
Mac
- Open the package file you downloaded and follow the prompts to install Go.
- The package should put the /usr/local/go/bin directory in your PATH environment variable. You may need to restart any open Terminal sessions for the change to take effect.
- Verify that you've installed Go by opening a command prompt and typing the following command:
$ go version- Confirm that the command prints the installed version of Go.
Linux
- Remove any previous Go installation by deleting the /usr/local/go folder (if it exists)
- Extract the archive you just downloaded into /usr/local, creating a fresh Go tree in /usr/local/go:
$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz
Warning: Do not untar the archive into an existing /usr/local/go tree. This is known to produce broken Go installations.
- Add /usr/local/go/bin to the PATH environment variable: You can do this by adding the following line to your $HOME/.profile or /etc/profile (for a system-wide installation):
export PATH=$PATH:/usr/local/go/bin
Note: Changes made to a profile file may not apply until the next time you log into your computer. To apply the changes immediately, just run the shell commands directly or execute them from the profile using a command such as source $HOME/.profile.
- Verify that you've installed Go by opening a command prompt and typing the following command:
$ go version
Confirm that the command prints the installed version of Go.
Source: https://go.dev/doc/install
GoLand by Jetbrains is an IDE from JetBrains that offers a dedicated and feature-rich experience for Go development. Since it's designed specifically for Go, it has a deep understanding of the language and provides quality code suggestions, refactoring support, and integrated tooling for debugging and testing. Students can use all of Jetbrains IDEs (including GoLand) for free with Github's Student Developer Pack for the length of their studies.
Vim Go is a keyboard based text editor that can be enhanced with plugins like vim-go. It's a great option for advanced users and experts but has a steep learning curve for beginners. It's ideal for those who prefer a terminal based workflow or a Unix style interface. Vim is fast, customizable, and good for rapid development.
VSCode by Microsoft is a powerful editor that supports multiple languages, making it easy to develop full stack Go applications without switching IDEs. It features a marketplace of extensions including an official Go extension, deep Git integration, an AI co-pilot, and more to make it a great all in one solution for doing front-end, back-end, databases, APIs, and Documentation all within the same workspace.
The Official Go VSCode extension provides features designed to help any beginner get started with Go, including IntelliSense code suggestions and semantic syntax highlighting. It also offers tools like hover information for detailed insights on keywords, variables, and structs, alongside efficient keyboard shortcuts for code navigation and file formatting. Developers also benefit from a custom Go test UI, as well as support for package import fixing, refactoring, and debugging. A complete list of features and explanations are available on the VSCode Go extension Github repository.
- Be sure you've installed Go on your computer and confirm its working.
- Download VSCode and install it on your preferred drive.
- Open VSCode after installing.
- Navigate to the Go extension page on Microsoft Marketplace and click install with VSCode open.
- It will ask you if you'd like to install the extension. Follow the prompts to install.
- Close your program and restart your computer.
Microsoft has provided a link to this helpful video by Google Open Studios on setting up your Go environment in VSCode and creating your first file.
Perequisites:
- Go installed and working in your console
- VSCode installed
- Go official VSCode extension installed
- Open Visual Studio Code and launch a new terminal using Terminal > New Terminal
- Create a folder for your project
mkdir my-first-go-project
- Go to the folder using cd
cd my-first-go-project
Or, navigate to it using File > Open Folder...
- Create your first module. This creates a go.mod file that tracks your project's dependencies.
go mod init my-first-go-project
- Create a package folder.
mkdir hello
- Navigate to the package folder you just created.
cd hello
- Create a file named main.go in the hello folder:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}- Run the file in the terminal
go run hello/main.go
Or click Run > Run Without Debugging
- Compile your file into an executable (Windows)
go build -o hello-app.exe hello/main.go
- Run your executable
On Mac/Linux
./hello-app
On Windows
.\hello-app.exe
The complete project structure should look like this:
my-first-go-project/
├── go.mod
├── hello/
│ └── main.go
└── hello-app.exe
View the full example here
For single line comments in Go, use two forward slashes.
Single line comments can be on their own line or they can append an existing line of code.
// This is a comment
package main
import "fmt"
func main() {
fmt.Println("Hello, World!") // This is also a comment
}For multi-line comments, use a forward slash asteriks, asteriks forward slash, with the comment in between.
/*
This is an example
of a multi-line comment
in Go
*/Go has an advanced feature call go directives that attach to comments in Go. Therefore, don't use //go: when making regular single line comments. For more information on go directives and other comment conventions, learn more in the Go Comments Documentation and Go Doc Comments.
- go.dev
- go.dev/play/ The Go playground for writing, running, and sharing code online.
- go.dev/doc/ The official documentation for the Go programming language.
- go.dev/doc/effective_go Official documentation of clean coding in Go.
- go.dev/doc/faq Frequently Asked Questions (and answers) about the Go language.
- go.dev/ref/spec The official Go language specification.
- go.dev/blog/ The official Go blog for news and in-depth article.
- pkg.go.dev The official Go package discovery and reference site.
- Golang.org
- github.com/golang The official GitHub organization for the Go project.
- Gobyexample.com
- Golangbot.com
- Practical-go-lessons.com
- Geeksforgeeks.org/go-language
- W3schools.com/go/
- go101.org
- Learn Go With Tests
- Spaceship Go by
- How to Code in Go by Digital Ocean
- Go from the Beginning by Chris Noring and Code Repository
- Anti-Textbook Go Book by (???) and Code Repository
Go is statically typed (as opposed to dynamically typed), similar to Java, C++, and Rust. This means the type of a variable is known and checked at compile time, unlike in Python, JavaScript, and Ruby, where types are determined and checked at runtime.
// Go
count := 10
count = "hello" // Error# Python
count = 10
count = "hello" # Valid in PythonHowever, Go supports type inference with the := operator. Go allows both explicit type declarations and implicit type inference. The compiler infers its type from the value you assign at compile time (not runtime) when you use the := operator, but once inferred, the variable type is fixed.
# Explicit
var count int = 10
# Implicit
count := 10 // The compiler infers this as an int
Because Go is strongly typed, the language prevents operations between incompatible types and does not perform implicit type conversions. For example, you cannot add a string and an integer. This is stricter than JavaScript (weakly typed) but similar to Python (which is also strongly typed). Learn more about type conversions below.
Variables declared with var or the short declaration operator := are mutable*. Their value can be changed.
x := 5
x = 10
However, constants declared with the reserved const keyword are immutable. Their value must be known at compile time and can't be changed.
const pi = 3.14
pi = 2.71 // Error
Note
Reference types like slices/maps/channels are always mutable.
bool can carry only the true or false value. It's default value is always false.
func main() {
var isSunny bool = true
var isRaining bool
if isSunny && !isRaining { // If it's sunny AND NOT raining
fmt.Println("Let's go outside.")
} else { // otherwise...
fmt.Println("Let's stay indoors.")
}
}
Signed and unsigned integers in Go have generic types and byte specific types. For instance, int is 64 bits on a 64 bit system. However, if you want to limit it to just 8, you could use int8. The same goes for unsigned integers.
Signed Integers can store both positive and negative values.
- int is generic and platform dependent. They are 32 bits in 32 bit systems and 64 bit in 64 bit systems.
- int8
- int16
- int32 is also a rune (see below)
- int64
Unsigned Integers cannot hold negative values.
- uint is also generic and platform dependent. They are 32 bits in 32 bit systems and 64 bit in 64 bit systems.
- uint8 is also a byte (see below)
- uint16
- uint32
- uint64
- uintptr is an unsigned integer type large enough to hold the bit pattern of any pointer. It is used in low-level programming with the unsafe package. It should be used with extreme caution. More on that here.
int and uint are implementation-dependent. This sometimes causes portability issues across 32 bit and 64 bit systems. Therefore its convention to specify which explicity in most cases or infer its type by value.
Warning
uintptr is NOT garbage collected.
Read more about integers from w3schools.com
Floating-Point Numbers are like floats in python, which are used for both 32 bit and 64 bit decimals numbers.
- float (without byte specification) defaults to float64 but in Go it's convention to specify the float type explicity or infer it by value.
- float32 is similar to floats in C++ and Java
- float64 is similar to doubles in C++ and Java
Complex Numbers are the set of all complex numbers with float real and imaginary parts
- complex64 are float 32 real and imaginary parts
- complex128 are float 64 real and imaginary parts
Learn more about numeric types on go.dev/ref/spec#Numeric_types.
In Go, a string is an immutable sequence of bytes that is interpreted as UTF-8 text.
There are a variety of ways to initialize a string in Go. The most common ways are:
var s1 string = "Hello, Go!"
s2 := "Hello, World!"Raw strings can span multiple lines. If you want to preserve all charachters in a raw string without escapes use single back ticks
raw := `Hello,
World.
\nThis is ignored.`
fmt.Println(raw)Once a string is created, it cannot be changed. Indexing a string returns a byte, not a full Unicode (rune). Slicing a string preserves the UTF-8 encoding so you can extract parts of it.
package main
import "fmt"
func main() {
text := "Hello"
fmt.Println("First byte:", text[0]) // 72
fmt.Println("Slice [0:1]:", text[0:1]) // H (UTF-8)
fmt.Println("Rune:", []rune(text)) // [72 101 108 108 111] (Unicode)
}To reduce confusion and help distinguish the intent of a value, Go has a few aliases that make handling data easier.
A rune is an alias for int32, a rune holds a full 32-bit Unicode character making it easy for working with non-ASCII text.
var r rune = '⌘'A byte is an alias for uint8, and is used for a variable meant to be a raw piece of 8-bit data like an ASCII character.
var b byte = 'a'An array is a fixed-length sequence of elements of a single type. These types are composed of elements or fields, which are themselves other types. Arrays cannot hold different types at the same time though, like python lists. For that functionality, see interfaces (below.)
Arrays can hold
- Basic types: int, float64, bool, string, etc.
- Composite types: struct, [n]Type (arrays), pointers, functions, etc.
- Empty interface [n]interface{} can hold any type (mixed types)
var numbers [5]int // An array of 5 integers initialized to [0 0 0 0 0]
var names [2]string{"Sally","Dave"} // An array of 2 strings initialized to [Sally, Dave]
A struct is a collection of named fields, where each field can be of a different type.
type Person struct {
Name string;
Age int
}Most of the labor done by classes in OOP languages like Java are done with structs in Go. A key difference is that Go separates the struct (data) from the methods or functions (behavior). Instead they are bound with a receiver (see binding below.) There is no inheritance, so Go uses struct embedding. Structs can be embedded into one another to create complex data relationships.
type Person struct {
Name string
Age int
}
type Employee struct {
Person // Embedded Person struct
EmployeeID string
}A pointer holds the memory address of a variable.
*int
*MyStructEach variable or object occupies storage space in memory. You can retrieve the address of a variable using the & operator:
var a int = 10
ptr := &a
fmt.Println(ptr)In Go, a function can also be a type, allowing functions to be passed as arguments and assigned to variables.
func(int, int) intGo uses interfaces to achieve polymorphism. An interface is a collection of method signatures, and any type that implements all the methods of an interface can be treated as that interface's type. This is different from class-based inheritance where a subclass must explicitly inherit from a superclass.
A variable of an interface type can hold any concrete value that implements all the methods in the interface.
Example: Empty Interface
var anyValue interface{}
anyValue = 42 // Can hold int
anyValue = "hello" // Can hold string
anyValue = []float64{1.2, 3.4} // Can hold sliceExample: Error Interface
type error interface {
Error() string
}
type MyError struct {
Message string
}
func (e MyError) Error() string {
return "Error: " + e.Message
}
Example: Stringer Interface
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}Slice ([]T) dynamic-sized, flexible view into an array. This is one of the most used data structures, replacing arrays for most use cases.
[]int
[]stringMap (map[K]V) is an unordered collection of key-value pairs similar to a python dictionary.
map[string]intChannel (chan T) is conduit for sending and receiving values with the arrow <- operator used for communication between goroutines (lightweight threads).
chan int, chan<- string (send-only), <-chan bool (receive-only)Go requires explicit type conversions between different types. Unlike some languages (e.g., JavaScript, Python, C), Go does not perform implicit coercion, even between numeric types. You must tell the compiler exactly how to convert one type to another using the syntax:
-
Numeric types: All conversions between numeric types are explicit (int to float64, float64 to int, int32 to int64, etc.) Overflow and truncation can occur silently.
-
String ↔ byte slice:
[]byte("hello")creates a slice of bytes from a string.string([]byte{104, 101, 108, 108, 111})converts bytes back into a string. -
Rune ↔ string: Converting a rune (int32) to string produces a one-character string containing the UTF-8 encoding of the rune.
-
Untyped constants: An untyped constant can be assigned to variables of different types without explicit conversion until it’s given a concrete type:
const n = 5
var a int32 = n
var b float64 = n
-
You cannot directly convert a string to an int; you must use helper functions like strconv.Atoi
-
Interfaces: A value of one type can be assigned to an interface if the type implements the interface methods — this is not a "conversion" but implicit interface assignment. To get the original type back, you need a type assertion:
var i interface{} = 42
v := i.(int)
Go has 25 reserved words that cannot be used as identifiers such as variable names.
- break
- case
- chan
- const
- continue
- default
- defer
- else
- fallthrough
- for
- func
- go
- goto
- if
- import
- interface
- map
- package
- range
- return
- select
- struct
- switch
- type
- var
- A variable name must begin with a letter or an underscore _. The remaining characters can be letters, digits, or underscores.
- Names are case-sensitive so myNum and MyNum are different variables.
- If an identifier needs to be visible outside its package (exported), it must start with a capital letter.
- In Go, identifiers that start with a capital letter are exported (public), while those starting with lowercase are unexported (package-private).
- The underscore _ blank identifier has a special role and is used to ignore values, e.g. in assignments or imports.
- You always have to specify either type or value (or both).
- Acronyms should be in all caps: ServeHTTP, urlAPI, etc.
- CamelCase is preferred.
- Don't use underscores for common variable names despite them being legal.
- Full meaningful words for variables specific to your program i.e. serverAWS not s1
- Use := only when introducing a new variable.
- Use = if you only want to reassign an existing variable.
- Single-method interfaces are often named with the method name plus -er: Reader, Writer, Stringer
- Exported (Public) Identifiers: If an identifier starts with a capital letter, it is exported (visible from outside its package). This is Go's mechanism for public visibility.
fmt.Println // Println is exported because it starts with 'P'.
http.ListenAndServe
- Unexported (Private) Identifiers: If an identifier starts with a lowercase letter, it is unexported and only accessible within the package it's declared in.
myHelperFunction
internalCounter
- Use common short words for readability. The more professional Go code you'll read, you'll notice some patterns appear often:
i, j, etc - used often in nested loops
n - for counts or number
p - pointer
r - io.Reader
w - io.Writer
rw - io.ReadWriter
err - error
db - database
cfg - config
Go supports composite literals, which provide a concise way to construct values for arrays, slices, maps, and structs.
arr := [3]int{1, 2, 3}s := []string{"a", "b", "c"}m := map[string]int{"one": 1, "two": 2}p := Person{Name: "Alice", Age: 30}
Go distinguishes between defined types and type aliases:
Defined type: creates a new, distinct type
type MyInt int
var x MyInt = 10
var y int = 20
Type alias: another name for an existing type
type MyIntAlias = int
var a MyIntAlias = 30
var b int = 40
Go has a standard set of C-like operators.
-
Arithmetic: +, -, *, /, %
-
Comparison: ==, !=, <, <=, >, >=
-
Logical: &&, ||, !
-
Bitwise: & (and), | (or), ^ (xor), &^ (and not), << (left shift), >> (right shift)
-
Assignment: =, +=, -=, *=, /=, %=, etc.
-
Address / Pointer: & (address of), * (dereference)
-
Channel: (used for sending/receiving from channels)
-
Increment/Decrement operators: ++, --
Go only allows increment/decrement as statements, not expressions"
i++ // is valid
x = i++ // is invalid
In Go, := can redeclare a variable if there’s at least one new variable being declared in the same statement.
This can lead to shadowing, where an inner variable hides an outer one.
package main
import "fmt"
func main() {
x := 10
fmt.Println("Outer x:", x)
if true {
x := 20 // 👈 Shadows the outer x
fmt.Println("Inner x:", x)
}
fmt.Println("Outer x again:", x) // Still 10, inner x is gone
}Go does not allow mixed-type operations without an explicit conversion. This is a core tenet of its strong typing.
var x int32 = 10
var y int64 = 20
// sum := x + y // Compile Error
sum := int64(x) + y // This worksThe one exception is that untyped constants (like const n = 5) can be mixed in expressions until they are assigned to a variable.
-
Numbers (int, float, complex): + - * / % (mod only for ints), comparisons (== != < <= > >=), bitwise ops (& | ^ &^ << >>, only for ints).
-
Strings: + (concatenation), comparisons (== != < <= > >= lex order).
-
Booleans: && || !, comparisons (== !=).
-
Pointers: * (dereference), & (address of), == != (compare addresses).
-
Interfaces: == != (two interfaces equal if both dynamic type and value are equal, or both nil).
-
Structs: == != only if all fields are comparable types.
-
Arrays: == != if element type is comparable.
-
Slices, Maps, Functions, Channels: only == != against nil.
-
Channels: additionally, <- for send/receive.
| Scenario | Example | Binding Time |
|---|---|---|
| Variable Declaration | var x int = 5 |
Compile time (the name x is bound to a variable object) |
| Short Variable Declaration | x := 10 |
Runtime (within compile constraints); the type and object are determined at compile time, but initialization happens at runtime |
| Function Declaration | func add(a, b int) int { return a + b } |
Compile time; the identifier add is bound to the function’s entry point |
| Constant Declaration | const Pi = 3.14 |
Compile time; immutable binding |
| Type Definition | type MyInt int |
Compile time; binds a new name to an existing or new type |
| Package Imports | import "fmt" |
Compile time; binds the identifier fmt to the imported package namespace |
| Interface and Method Bindings | Interface methods bound to concrete types | Runtime (dynamic dispatch) when the interface is assigned a concrete value |
-
Stack Allocation: Local variables that don’t escape their function are stored on the stack.
-
Heap Allocation: Variables that “escape” (e.g., returned by a function or captured by a closure) are stored on the heap, managed by the garbage collector.
-
Addressability: Not all expressions have addresses (e.g., constants, temporary values, and function results are not addressable).
Static Lifetime applies to package level variables and exist for the duration of the program:
var counter int
Local Lifetime variables exist only during the execution of their containing function:
func demo() {
x := 5
}
Heap Lifetime variables that returned references exist until garbage is collected:
func makeCounter() *int {
c := 0
return &c
}
-
Universe Scope: Built-in identifiers available everywhere ex.
int, true, len -
Package Scope: Identifiers declared at the top level of a package file
-
File Scope: Applies to imports and variables declared in a single file
-
Function Scope: Names declared inside a function are visible only there
-
Block Scope: Identifiers within {} are visible only within that specific block
var global = "package scope"
func demo() {
local := "function scope"
{
inner := "block scope"
fmt.Println(inner)
}
// fmt.Println(inner) // Error
}
- := cannot be used at package level; must declare at least one new variable.
- const only for primitive values; cannot be slice/map/channel.
- Go’s enumerations rely on iota. That’s part of how constants are typically used in practice.
- untyped constants. These are more flexible than variables until assigned a type.
const n = 5
var x int32 = n // allowed
var y float64 = n // also allowed
- Arrays/structs: == only if elements/fields are comparable.
- Slices/maps/functions: cannot be compared except to nil.
- Must use explicit conversion for mixed numeric types (e.g., int + float64).
- Arrays and slices are homogeneous — cannot store different types unless using interface{}.
- Type conversion syntax (T(v)) is explicit and limited — some require helper functions (strconv.Atoi for string → int).
- No operator overloading, no implicit type coercion.
- Function equality → only comparable against nil (not against other functions).
- Zero values: variables are auto-initialized (0, "", nil, false) — can cause logic errors if assumptions differ.
- Slices and maps are reference types: assigning them copies the reference, not the underlying data.
- Nil interfaces: (type, value) pairs — an interface holding a typed nil is not equal to nil.
- nil is the zero value for reference types (slice, map, channel, pointer, interface, function).
- But their behavior differs: nil slices can still be appended to, while nil maps panic on write.
- Shadowing with := can silently redefine outer variables.
As mentioned under the DataTypes section, Boolean takes only true and false which is important to remember as we discuss conditionals.
Go officially supports if/else and switch statements. Unlike C++/Java, Go has no official else if keyword. Also, Go doesn't use parenthesis around control statements like C++ and writes them directly, similar to Python.
if x == true {
fmt.Println("x is true")
} else {
fmt.Println("x is false")
}
if x > 0 && y < 10 {
fmt.Println("Both conditions met")
}Else if is emulated using else followed by if which, is a nuanced distinction.
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}Source: (Go Documentation Spec. - If Statements)[https://go.dev/ref/spec#If_statements]
Go has only for loop that replaces while, do/while, and foreach from other languages.
for i := 0; i < 5; i++ {
fmt.Println(i)
}To emulate a while loop, Go is smart and recognizes when a condition is not met.
x := 5
for x > 0 {
fmt.Println(x)
x--
}Example emulation of a for each
numbers := []int{1, 2, 3}
for index, value := range numbers {
fmt.Printf("%d: %d\n", index, value)
}Go also can print and infinite loop if not careful.
for {
fmt.Println("Forever")
break
}Inside a for loop, you can use break to stop it.
for x := 0; n <= 10; x++ {
if x == 4 {
break
}
fmt.Println(x)
}Continue only stops the execution of the current iteration. It continues with the next one.
for x := 0; x <= 10; x++ {
if x%2 == 0 {
continue
}
fmt.Println(x)
}range here iterates through the numbers slice
for i, num := range numbers {
if num > 25 {
fmt.Printf("Index %d: %d is greater than 25\n", i, num)
}
}Labels can be used with break and continue to control exactly from which loop we want to break or continue
func main() {
ControlBreak:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
fmt.Println(i)
break ControlBreak
}
}
}Source: Exercism -- For Loops
Go's switch automatically breaks after each case. It also accepts multiple values!
switch day {
case "Monday":
fmt.Println("Start of week")
case "Friday":
fmt.Println("Almost weekend")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}The word ```fallthrough`` is used to cause the switch to go to the next case.
switch number {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
fallthrough // <<< GO TO THE NEXT CASE
case 3:
fmt.Println("Three")
default:
fmt.Println("Other number")
}Source: Go.dev -- Switch Statements Go.dev -- Fallthrough Statements
Go uses explicit braces {} for code blocks. This pevents the danging else issue. Opening braces must be on the same line, never the next line. It doesn't use semicolons.
if x > 5 {
fmt.Println("Braces Always Required)
}Go uses short-circuit evaluation like Java/C++. Logical operators && and || evaluate left-to-right.
package main
import "fmt"
func main() {
if false && printEx() {
// This won't run
}
if true || printEx() {
fmt.Println("Done")
}
}
func printEx() bool {
fmt.Println("This won't run either")
return true
}Source: Go.dev -- Operators
GGo omits many features like ternary operator, parentheses around conditions, while loops, and inheritance) because its primary design goal is simplicity. As Rob Pike (Go co-designer) stated: "The key point here is our programmers are Googlers, they're not researchers..." So the designers believe simpler is more maintainable.
Readability over writeability - Code should be clear to readers not writers
Simplicity over complexity - Fewer features mean fewer bugs
Explicit over implicit - Nothing magical that's hard to trace
Compilation speed - Minimal features enable fast builds
Source: Go at Google: Language Design in the Service of Software Engineering
In Go, functions are declared using the func keyword followed by a name, parameters in parentheses, and optional return types. Functions can accept multiple parameters of different types, and parameters can be grouped that share the same type declaration.
Go is compiled, so function order doesn't matter. There are no specific function placement rules. Functions can be declared anywhere in the package, above or below a function call.
Source: Go - Function declarations
Example with multiple parameters:
func add(a int, b int) int {
return a + b
}Example with different data types:
func printInfo(name string, age int) {
fmt.Println(name, age)
}Example with parameters grouped:
func multiply(x, y int) int {
return x * y
}Go controls visibility through capitalization rules where uppercase names are public and lowercase are private.
Source: Go Blog
Example:
func PublicFunc() {} // Accessible outside package
func privateFunc() {} // Accessible in this packageA special feature of Go is that functions can return multiple values, which should be specified after the parameters, and these return values can be named.
Source: Go - Function types
Example returning multiple values at the same time:
func getName() (string, string) {
return "Sally", "Sue"
}For flexibility, functions can be created that accept a variable number of arguments using the ... syntax on the final parameter.
Source: Go - Function types
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}Functions in Go can be attached to types as methods using receivers, written without names as anonymous functions, and return other functions.
Source: Go - Method declarations
type Rectangle struct { width, height int }
func (r Rectangle) area() int {
return r.width * r.height
}
func main() {
square := func(x int) int { return x * x }
}Go also provides helpful features like the defer keyword for cleanup actions.
Source: Go Blog - Defer, Panic, and Recover
func example() {
defer fmt.Println("Done")
fmt.Println("Working...")
}Unlike Python's function-level scope and Java's block-level scope with hoisting, Go uses strict block-level scope without hoisting, meaning variables exist only within their declared blocks and aren't accessible before they're declared.
| Scope Level | Visibility | Lifetime | Unusual Characteristics |
|---|---|---|---|
| Universe | Predeclared identifiers | Entire program | Built-in types and functions like int, println |
| Package | Exported identifiers across files | Entire program | Capitalized names are accessible outside package |
| File | Imported packages | File duration | Imports are file-scoped, NOT package-wide |
| Function | Parameters, local variables | Function execution | Can shadow package-level variables |
| Block | Variables declared in blocks | Block execution | Includes if, for, switch statements, etc. |
Example Code:
var global = "global"
func main() {
local := "local"
if true {
block := "block"
fmt.Println(global, local, block) // Accessible
}
// fmt.Println(block) // Error: block not accessible
}Source: Go - Declarations & Scope
Source: Effective Go
Go is pass-by-value by default. When you pass arguments to functions, Go creates copies of the values. When you pass pointers, slices, maps, or channels, you're copying the reference, allowing modification of the original data.
Source: Go FAQ - Functions
Pass-by-Value
func doubleValue(x int) {
x = x * 2
}
func main() {
num := 5
doubleValue(num)
fmt.Println(num) // 5 - unchanged
}Pass-by-Reference
func doubleReference(x *int) {
*x = *x * 2
}
func main() {
num := 5
doubleReference(&num)
fmt.Println(num) // 10 - modified
}Side effects occur when functions modify data outside their local scope. Go allows side effects through pointers, but provides protection with arrays by passing them as copies. This prevents accidental modifications to original array data.
package main
import "fmt"
func main() {
nums := [3]int{1, 2, 3}
modifyArray(nums)
fmt.Println(nums) // [1 2 3] - unchanged
}
func modifyArray(arr [3]int) {
arr[0] = 99
fmt.Println(arr) // [99 2 3] - only local copy changed
}Source: Go - Assignments
Go has built-in protections against side effects through its pass-by-value behavior for basic types and arrays. When you pass arrays and basic types to functions, Go creates copies, preventing accidental modifications to the original data.
package main
import "fmt"
func main() {
data := [2]int{1, 2}
update(data) // Array is copied
fmt.Println(data) // [1 2] NOT changes
}
func update(d [2]int) {
d[0] = 99 // Modifies only the copy
}Go automatically manages memory using two primary areas: the stack and the heap. The stack provides fast access for short-lived data like function arguments and local variables, while the heap stores data that needs to persist longer or is shared across function boundaries. Go uses escape analysis to determine where to store variables. If a variable's lifetime extends beyond its function, it "escapes" to the heap.
Function Storage:
- Stack: Function arguments, parameters, return addresses, and most local variables
- Heap: Data that escapes function scope, large allocations, and shared data
During Execution:
- Local Variables: Typically on stack, unless returned or shared (then heap)
- Arguments: Copied onto stack when function is called
- Parameters: Stored on stack as local variables within the function
Source: Go Blog - Escape Analysis Source: Go Memory Management
Recursive functions are functions that call themselves to solve smaller instances of the same problem. Each recursive call creates a new stack frame with its own parameters and local variables.
func factorial(n int) int {
if n <= 1 {
return 1 // Base case stops recursion
}
return n * factorial(n-1) // Recursive call
}
func main() {
result := factorial(5)
fmt.Println(result)
}Go uses structs as the primary building blocks for creating object-like data structures, and it uses methods that can be associated with any type (including structs). Structs are declared with the keywords type and struct. The dot operator . can be used to access struct attributes and methods.
type Person struct {
FirstName string
LastName string
Age int
}
func (p Person) FullName() string {
return p.FirstName + " " + p.LastName
}
func (p Person) Introduce() string {
return fmt.Sprintf("Hello, I'm %s and I'm %d years old", p.FullName(), p.Age)
}- Structs/Types: PascalCase such as
PersonorHttpClient - Fields/Methods: PascalCase for exported (public) fields/methods and camelCase for unexported (private) ones
- Methods: Same naming convention as functions
- Packages: lowercase, single-word names
Go does NOT have a set of standard methods that are automatically available on all types like Java or C# do. Go instead uses interfaces to define standard behaviors that types can implement optionally. Different than traditional OOP like Java, interface implementation is implicit. Types don't declare what interfaces they implement. They just need to have the required methods. And the compiler verifies interface implementation at compile time. Some examples are:
Stringer interface is the toString() Equivalent in Go.
// This is a built-in interface in the fmt package
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Person OPTIONALLY implements Stringer
func (p Person) String() string {
return fmt.Sprintf("Person{Name: %s, Age: %d}", p.Name, p.Age)
}
Error is used for standard error handling.
// Standard interface for errors
type error interface {
Error() string
}
// Custom error struct type
type ValidationError struct {
Field string
Message string
}
// Using the error interface
func (v ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", v.Field, v.Message)
}
// This is in io package
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
| Aspect | Function | Method |
|---|---|---|
| Declaration | func FunctionName(params) |
func (receiver) MethodName(params) |
| Calling | FunctionName(args) |
variable.MethodName(args) |
| Association | Standalone | Associated with a type |
Go doesn't have classical inheritance. Instead, it uses composition which is embedding one struct within another, and interfaces for defining behavior contracts. Go deliberately avoids multiple inheritance to keep the language simple. However, Go can achieve similar effects through implementing multiple interfaces, and multiple struct embedding through careful design.
type Animal struct {
Name string
}
type Dog struct {
Animal // Embedding for mock inheritance
Breed string
}
package main
import "fmt"
// This is the base type
type Writer struct {
Tool string
}
func (w Writer) Write() string {
return "Writing with " + w.Tool // A method for the writer to write with a tool
}
// Another base type
type Speaker struct {
Language string
}
func (s Speaker) Speak() string {
return "Speaking " + s.Language
}
// Mock multiple inheritance
type Author struct { // Make an Author
Writer // An Author is a writer
Speaker // The Author is also a speaker
Books int // custom field
}
func main() {
author := Author{
Writer: Writer{Tool: "pen"}, // This author writes with a pen
Speaker: Speaker{Language: "English"}, // This author speaks english
Books: 5, // This author has 5 books
}
// Print it out
fmt.Println(author.Write()) // From Writer
fmt.Println(author.Speak()) // From Speaker
// The custom field
fmt.Println("Books:", author.Books)
}
Go doesn't support method overloading such as multiple methods with the same name but different parameters. It handles method overriding through embedding.
type Base struct {
Value string
}
func (b Base) Display() string {
return "Base: " + b.Value
}
type Derived struct {
Base
Extra string
}
// This overrides the Base method
func (d Derived) Display() string {
return "Derived: " + d.Value + ", Extra: " + d.Extra
}
Value receivers operate on a duplicate copy of the object. This means any modifications made within the method only affect the temporary copy. The copy is erased once the method completes. In contrast, pointer receivers, work directly on the original object's memory location, so any changes the method makes will permanently alter the actual object itself. This important difference means it's convention to use value receivers when you only need to read or compute from the data which also makes things safer. Use pointer receivers when you need the method to update the object's state persistently.
type Counter struct {
count int
}
// Value receiver works on a copy
func (c Counter) IncrementValue() {
c.count++ // THIS ONLY CHANGES THE COPY
}
// Pointer receiver works on the ORIGINAL
func (c *Counter) IncrementPointer() {
c.count++ // WARNING! Changes the actual object!!!
}
func main() {
c := Counter{count: 0}
c.IncrementValue()
fmt.Println(c.count) // 0 (unchanged)
c.IncrementPointer()
fmt.Println(c.count) // 1 (changed)
}
- Go doesn't have classes. Instead, you can attach methods to any type: structs, basic types, etc.
- A type automatically implements an interface if it has all the required methods and no explicit declaration is needed
- The empty interface like interface{} can hold any type, similar to Object in Java.




