Basics

Go Programming Basics #

Essential Concepts for Newcomers

Questions? #

  1. What is Go, and what types of projects is it best suited for?
  2. Explain Go’s type system and how it differs from other popular languages.
  3. Explain pass-by-value semantics and how reference types work in Go.
  4. What are Go interfaces, and why are they important?
  5. What’s the difference between nil interfaces and empty interfaces in Go?
  6. How do you implement polymorphism in Go?
  7. What are variadic functions in Go, and when should they be used?
  8. What is the iota keyword, and how is it used in Go?
  9. Compare slices vs arrays

Answers: #

1. What is Go, and what types of projects is it best suited for? #

Go, also known as Golang, is a statically typed, compiled language designed for simplicity, performance, and strong concurrency support. It excels in scenarios requiring high efficiency and scalability. Some of the key areas where Go shines include:

  • Web servers, microservices, and APIs: Go’s standard library provides powerful tools for building HTTP servers and clients, making it ideal for backend development
  • Cloud-native and distributed systems: Its lightweight design and scalability make Go a natural fit for creating containerized applications and deploying them in cloud environments
  • DevOps tools and infrastructure: Tools like Docker and Kubernetes are written in Go due to its speed, portability, and ease of deployment

Go features a statically typed system, meaning variable types are determined at compile time. This ensures predictability and reduces the risk of runtime errors, making it distinct from dynamically typed languages. Key differences include:

  • No class-based inheritance: Instead of traditional object-oriented programming with classes, Go relies on interfaces and composition for structuring code. This approach is simpler and promotes flexibility
  • Strong typing: Go enforces strict type rules, requiring explicit conversions between types, unlike languages like JavaScript where implicit type coercion is common. While Go doesn’t support advanced type features like sum types, it introduced generics in version 1.18 to enhance type safety and flexibility

3. Explain pass-by-value semantics and how reference types work in Go. #

In Go, all variables are passed by value. This means when you pass a variable to a function, Go creates a copy of that variable. However, the behavior differs between value types (e.g., integers, structs) and reference types (e.g., slices, maps, channels) due to how they store data. For Value Types (int, float, bool, string, structs and arrays) a full copy of the data is made.

Reference Types (Slices, Maps, Channels) #

Reference types internally hold a pointer to an underlying data structure. When passed to a function:

  • The header (e.g., slice length/capacity, map pointer) is copied
  • The underlying data is shared
  • Slices: The slice header (pointer to array + length/capacity) is copied, but both headers point to the same array
  • Maps: The map header (pointer to hash table) is copied, but both point to the same data
OperationAffects Original?Why
Modify elementsYesShared underlying data (array, hash table, etc.)
Reassign variableNoOnly modifies the local copy of the header
Append/resizeNo*Creates new data for local copy (unless returned and reassigned)

When to Use Pointers #

Use pointers explicitly to:

  1. Modify the header (e.g., slice length/capacity):
func growSlice(s *[]int) {
    *s = append(*s, 100)
}
  1. Avoid copying large structs:
func processBigStruct(b *BigStruct) { ... }

Key Takeaways #

  • Go always passes copies of variables
  • Reference types (slices, maps, channels) share underlying data but have isolated headers
  • To modify headers (e.g., slice length), return the modified value or use pointers
  • Changes to elements are visible globally; changes to headers are local

This design balances efficiency (no deep copying) with safety (no unintended side effects from header changes).


4. What are Go interfaces, and why are they important? #

Go’s interfaces provide a unique form of type abstraction, distinct from those in languages like Java. In Go, interfaces are implicitly implemented, meaning a type satisfies an interface simply by implementing its methods, without needing to explicitly declare that it implements the interface. Go embraces the philosophy of “duck typing”: if it quacks like a duck, then it’s a duck. If a type provides all the methods defined by an interface, it is considered to implement that interface. This approach allows for highly dynamic and adaptable code.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type MyReader struct{}

func (m MyReader) Read(p []byte) (n int, err error) {
    // implementation
}

In this example, MyReader implicitly implements the Reader interface because it defines the Read method. No explicit declaration is required.

Go interfaces are collections of method signatures that define a set of behaviors for types. They are important for several reasons:

  • Implicit Implementation: This design makes Go’s interfaces lightweight and highly flexible, encouraging loose coupling and simplifying code maintenance
  • Polymorphism: Interfaces enable polymorphic behavior, allowing different types to be used interchangeably as long as they implement the required methods
  • Decoupling: Interfaces help reduce dependencies between different parts of the codebase, promoting more modular and flexible designs
  • Code reusability: By using interfaces, developers can write more generic code that works with any type implementing the interface, reducing code duplication
  • Testing: Interfaces make it easier to create mock objects for unit testing, improving testability of code
  • Composition over inheritance: Interfaces in Go encourage composition rather than hierarchical inheritance, leading to more flexible and maintainable code structures
  • Late abstraction: Go’s interface design allows developers to define abstractions as they become apparent, rather than forcing early decisions about type hierarchies
  • Reflection and type assertions: Interfaces enable runtime type inspection and manipulation through reflection and type assertions

Interfaces in Go provide a powerful tool for creating clean, modular, and extensible code by defining behavior contracts that types can fulfill without explicit declarations.


5. What’s the difference between nil interfaces and empty interfaces in Go? #

Understanding the distinction between nil interfaces and empty interfaces is crucial for writing correct Go code and avoiding subtle bugs.

Nil Interfaces #

A nil interface has both its type and value set to nil:

  • It’s the zero value of an interface type
  • Checking for nil directly (if x == nil) works as expected
  • Created when you declare an interface variable without assigning a value
var w io.Writer  // nil interface (type=nil, value=nil)
fmt.Println(w == nil)  // true

Empty Interfaces #

An empty interface (interface{} or any in Go 1.18+) can hold values of any type:

  • It may contain a nil value of a concrete type
  • But the interface itself is NOT nil (it has a type)
  • Direct nil checks can be misleading

The Nil Interface Trap #

This is one of Go’s most common gotchas:

func main() {
    var p *int = nil           // nil pointer
    var i interface{} = p      // empty interface holding nil pointer

    fmt.Println(p == nil)      // true
    fmt.Println(i == nil)      // false! (interface has type *int, value nil)
}

6. How do you implement polymorphism in Go? #

Go implements polymorphism through two main approaches:

  1. Runtime polymorphism via interfaces (traditional approach since Go 1.0)
  2. Compile-time polymorphism via generics (introduced in Go 1.18)

Interface-based Polymorphism (Runtime) #

This is the most common form of polymorphism in Go:

  • Interface-based polymorphism: Define interfaces that specify a set of methods, then implement these interfaces in different types. This allows for flexible code that can work with any type adhering to the interface contract
  • Type assertions and type switches: These mechanisms allow for runtime type checking and branching based on concrete types, enabling polymorphic behavior
  • Empty interface (interface{} or any): This can be used to accept any type, providing a form of polymorphism at the cost of type safety
  • Function values and closures: These can be used to create polymorphic behavior by passing functions as arguments or returning them from other functions
  • Embedding: Struct embedding allows for a form of composition that can achieve some polymorphic behaviors

Generic Polymorphism (Compile-time) #

Go 1.18+ supports generics for compile-time polymorphism:

// Generic function working with any comparable type
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return true
}

// Works with different types at compile time
Contains([]int{1, 2, 3}, 2)
Contains([]string{"a", "b"}, "c")

Both approaches have their use cases: interfaces provide runtime flexibility and dynamic dispatch, while generics offer type-safe code reuse with compile-time type checking


7. What are variadic functions in Go, and when should they be used? #

Variadic functions in Go are functions that can accept a variable number of arguments of the same type. They are defined using an ellipsis (...) before the type of the last parameter in the function signature.

Key characteristics of variadic functions:

  • They allow passing an arbitrary number of arguments, including zero
  • The variadic parameter must be the last one in the function definition
  • Internally, Go treats the variadic arguments as a slice of the specified type

When to use variadic functions:

  • To accept an arbitrary number of arguments without creating a temporary slice
  • When the number of input parameters is unknown at compile time
  • To improve code readability and create more flexible APIs
  • To simulate optional arguments in function calls

Examples of variadic functions in Go include fmt.Println() and custom functions like:

func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }

    return total
}

This function can be called with any number of integer arguments:

sum(1, 2, 3)
sum(10, 20)
sum()

8. What is the iota keyword, and how is it used in Go? #

The iota keyword in Go is a special identifier used in constant declarations to create a sequence of related constants with incrementing values. Here are the key points about iota:

  1. It generates integer constants starting from 0 and incrementing by 1 for each subsequent constant within a const block
  2. iota resets to 0 whenever the const keyword appears in the source code
  3. It’s commonly used to create enumerations or sets of related constants
  4. iota can be used in expressions, allowing for more complex constant definitions

Example:

const (
    Monday = iota    // 0
    Tuesday          // 1
    Wednesday        // 2
    Thursday         // 3
    Friday           // 4
)

iota can also be used in more complex expressions:

const (
    KB = 1 << (10 * iota)  // 1 << (10 * 0) = 1
    MB                     // 1 << (10 * 1) = 1024
    GB                     // 1 << (10 * 2) = 1048576
)

Using iota simplifies the creation of related constants, making the code more maintainable and less prone to errors when defining sequences of values


9. Compare slices vs arrays #

Go provides both arrays and slices for working with sequences of elements. Understanding their differences is crucial for writing efficient Go code.

Arrays #

Fixed Size:

  • Arrays have a compile-time fixed size that’s part of their type
  • [5]int and [10]int are different types
  • Cannot be resized after declaration

Value Semantics:

  • Arrays are value types; copying an array copies all elements
  • Passing arrays to functions creates a full copy

Stack Allocation:

  • Small arrays are typically allocated on the stack
  • More efficient for small, fixed-size collections

Example:

var arr [5]int = [5]int{1, 2, 3, 4, 5}
arr2 := arr // Creates a complete copy
arr2[0] = 99
fmt.Println(arr[0])  // Output: 1 (unchanged)

Slices #

Dynamic Size:

  • Slices can grow or shrink dynamically
  • All slices of the same element type share the same type []int
  • Can be resized using append()

Reference Semantics:

  • Slices are reference types pointing to an underlying array
  • Copying a slice copies only the slice header (pointer, length, capacity)
  • Multiple slices can reference the same underlying array

Heap Allocation:

  • The underlying array is typically allocated on the heap
  • More flexible but with slightly more overhead

Example:

slc := []int{1, 2, 3, 4, 5}
slc2 := slc // Shares underlying array
slc2[0] = 99
fmt.Println(slc[0])  // Output: 99 (modified!)

Comparison Table #

FeatureArraySlice
SizeFixed at compile timeDynamic
TypeSize is part of typeSize not part of type
Passing to functionCopies all elementsCopies only header (~24 bytes)
MemoryUsually stackUsually heap
ResizableNoYes (via append)
Common usageRareVery common

When to Use Each #

Use Arrays when:

  • Size is known at compile time and never changes
  • You need value semantics (full copies)
  • Working with small, fixed collections
  • Performance-critical code needing stack allocation

Use Slices when:

  • Size is dynamic or unknown at compile time
  • You want to avoid copying large data structures
  • Building flexible APIs
  • Most general-purpose programming (default choice)

Key Takeaway #

In practice, slices are used far more often than arrays in Go code due to their flexibility. Arrays are mainly used when you need fixed-size buffers or want to guarantee value semantics.