Skip to content

Simmons University CS330 Fall 2025 repository for assignment tutorials and projects. For more information or commentary, see the discussion area.

Notifications You must be signed in to change notification settings

omicreativedev/go-quickstart

Repository files navigation

Welcome to Go Quickstart

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


Table of Contents

Part 1

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

Part 2

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

Part 3

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?

Part 4

Function Declaration & Syntax
Function Scope
Function Passby
      Side Effects
      Guardrails
Memory Management
      Recursive Functions

Part 5

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


Part 1

History of the Go Programming Language

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.

Important Features of Go

  • 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.

What is Go good for?

  • 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

Famous Projects Created with Go

  • 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.

Installation and Setup

Installing Go

The first step is to download and install Go. You can get the download here.

Windows
  1. Open the MSI file you downloaded and follow the prompts to install Go.
  2. You can change the location of your installation as needed. After installing, you will need to close and reopen any open command prompts.
  3. 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.
  4. In the Command Prompt window that appears, type the following command:
> go version
  1. Confirm that the command prints the installed version of Go.
Mac
  1. Open the package file you downloaded and follow the prompts to install Go.
  2. 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.
  3. Verify that you've installed Go by opening a command prompt and typing the following command:
$ go version
  1. Confirm that the command prints the installed version of Go.
Linux
  1. Remove any previous Go installation by deleting the /usr/local/go folder (if it exists)
  2. 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.

  1. 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.

  1. 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

Choosing an IDE

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.

Installing VSCode Go Extension

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.

  1. Be sure you've installed Go on your computer and confirm its working.
  2. Download VSCode and install it on your preferred drive.
  3. Open VSCode after installing.
  4. Navigate to the Go extension page on Microsoft Marketplace and click install with VSCode open.
  5. It will ask you if you'd like to install the extension. Follow the prompts to install.
  6. 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.

Creating Your First Go Project in VSCode

Perequisites:

  • Go installed and working in your console
  • VSCode installed
  • Go official VSCode extension installed
  1. Open Visual Studio Code and launch a new terminal using Terminal > New Terminal
  2. Create a folder for your project
mkdir my-first-go-project
  1. Go to the folder using cd
cd my-first-go-project

Or, navigate to it using File > Open Folder...

  1. Create your first module. This creates a go.mod file that tracks your project's dependencies.
go mod init my-first-go-project
  1. Create a package folder.
mkdir hello
  1. Navigate to the package folder you just created.
cd hello
  1. Create a file named main.go in the hello folder:
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
  1. Run the file in the terminal
go run hello/main.go

Or click Run > Run Without Debugging

  1. Compile your file into an executable (Windows)
go build -o hello-app.exe hello/main.go
  1. 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

Commenting Your Code

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 Learning Resources

Official Sources

Websites

Tutorials

Books

Videos

Other


Part 2

Type System and Variable Semantics

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 Python

However, 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.

Data Types

Basic Types

Boolean

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.")
	}
}

Numeric

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.

Strings

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)
}

Aliases

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'

Composite Types

Aggregate

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
}

Reference

A pointer holds the memory address of a variable.

*int
*MyStruct

Each 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)

Function Type

In Go, a function can also be a type, allowing functions to be passed as arguments and assigned to variables.

func(int, int) int

Interface Types

Go 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 slice

Example: 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)
}

Complex Types

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
[]string

Map (map[K]V) is an unordered collection of key-value pairs similar to a python dictionary.

map[string]int

Channel (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)

Type Conversion

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

Invalid conversions

  • 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) 

Syntax

Reserved Words

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

Variable Naming Requirements & Conventions

Required by Compiler

  • 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).

Encouraged by Professional and Community Standards

  • 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

Composite Literals

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}

Type Aliases vs. Defined Types

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  

Operators

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 works

The one exception is that untyped constants (like const n = 5) can be mixed in expressions until they are assigned to a variable.

Availability

  • 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.

Binding

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

Storage, Addresses, and Lifetime

  • 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
}

Scope

  • 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
}

Limitations

  • := 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).

Pitfalls

  • 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.

Part 3

Control Statements: Loops, Selection, Conditionals

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/else

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]

For Loops

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
}

Continue and 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

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

Switch Statements

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

Block Delimiting

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)
}

Source:

Short Curcuit Evaluation

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

Why Is Go Missing Things?

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


Part 4

Function Declaration Syntax

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 package

A 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...")
}

Function Scope

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 Hierarchy

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

Pass By

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

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

Guardrails

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
}

Memory Management: Stack vs Heap

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

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) 
}

Part 5

Go Programming Language Approach to Objects

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)
}

Naming Conventions

  • Structs/Types: PascalCase such as Person or HttpClient
  • 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

Standard Methods in Go

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

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

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)
}

Reader / Writer

// 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)
}

Functions vs. Methods in Go

Aspect Function Method
Declaration func FunctionName(params) func (receiver) MethodName(params)
Calling FunctionName(args) variable.MethodName(args)
Association Standalone Associated with a type

Inheritance

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
}

Mock Multiple Inheritance

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)
}

Overloading Method Names

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 vs Pointer Receivers

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)
}

Other Considerations

  • 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.

Footnotes

  1. Go FAQ: History

  2. Go FAQ: Creating a New Language

  3. The Go Programming Language Specification (Preface)

  4. Official Go Announcement

  5. Golang #3: No Classes, No Inheritance — Just Structs & Methods, and It Works

  6. Why doesn’t Go have Exceptions?

  7. Generic Map, Filter and Reduce in Go

  8. Go Blog: Share Memory by Communicating

  9. On Golang, “static” binaries, cross-compiling and plugins

  10. Mastering Error Handling in Go: A Guide to Explicit Error Handling

  11. Why did Golang Choose Composition as it’s base rather than Inheritance?

  12. The How and Why of Go, Part 1: Tooling

  13. A Guide to the Go Garbage Collector

  14. Tutorial: Getting Started with Generics

  15. Go for Cloud and Networked Services

  16. Command Line Interfaces in Go

  17. Development Operations & Site Reliability Engineering

  18. Building Restful API Go Tutorial

  19. Go Concurancy and Parallelism

  20. Go Pipeline Patterns

  21. Databases Implemented in Go

  22. Cyptography in Go

  23. 10 Programming Languages for Cybersecurity

  24. Go Programming for Embedded Systems

  25. Microcontroller Development with Golang

  26. Embeddable Scripting Languages in Go

  27. 20 Libraries for Automation in Golang

  28. Building a Proxy Server with Golang

  29. Built My Own Load Balancer in Go

About

Simmons University CS330 Fall 2025 repository for assignment tutorials and projects. For more information or commentary, see the discussion area.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages