Basics

Go Programming Basics #

Must-Know 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. How do you implement polymorphism in Go?
  6. What’s the difference between nil interfaces and empty interfaces in Go? How do you handle type assertions safely?
  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. How do you implement polymorphism in Go? #

Go implements polymorphism primarily through interfaces, without using generics. This approach is known as runtime polymorphism. Here are the key ways to achieve it:

  • 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{}): 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

These techniques allow Go to support polymorphism without the need for generics, maintaining the language’s focus on simplicity and compile-time type safety


6. What’s the difference between nil interfaces and empty interfaces in Go? How do you handle type assertions safely? #

The difference between nil interfaces and empty interfaces in Go is subtle but important:

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 (e.g., if x == nil) works as expected.

Empty interfaces #

  • An empty interface (interface{}) can hold values of any type.
  • It may contain a nil value of a concrete type, but the interface itself is not nil.
  • Direct nil checks can be misleading.

To handle type assertions safely: #

  • Use the two-value form of type assertion:

    value, ok := x.(Type)
    if ok {
        // Type assertion succeeded
    } else {
        // Type assertion failed
    }
    
  • Use type switches for multiple possible types:

    switch v := x.(type) {
    case string:
        // v is a string
    case int:
        // v is an int
    default:
        // unknown type
    }
    
  • For nil checks on interfaces, use reflection:

    func IsNil(value interface{}) bool {
        return reflect.ValueOf(value).IsNil()
    }
    

These methods help prevent panics and provide safer type conversions when working with interfaces.


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

Variadic functions provide a clean and elegant way to handle functions with a variable number of arguments.


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 usage:

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 #

Slices in Go are a flexible and dynamic way to work with collections of elements. They are far more common in application code than arrays due to their versatility and dynamic nature. Here’s how slices differ from arrays:

Dynamic Size #

Unlike arrays, slices can grow or shrink dynamically. They act as references to an underlying array, allowing operations like resizing without needing to define a fixed size upfront. This makes slices more adaptable for scenarios where the number of elements is not known beforehand.

arr := [5]int{1, 2, 3, 4, 5} // Array with fixed size
slc := arr[:3]               // Slice referencing the first 3 elements

Underlying Array #

Slices do not store data themselves; instead, they point to an underlying array. This design makes them efficient for passing large collections of data between functions without copying the entire dataset.

For example:

func modify(s []int) {
    s[0] = 99 // Modifies the shared underlying array
}

arr := [5]int{1, 2, 3, 4, 5}
slc := arr[:]
modify(slc)
fmt.Println(arr) // Output: [99 2 3 4 5]

Slices are lightweight and powerful tools in Go, enabling dynamic operations while maintaining efficiency through shared memory with their backing arrays.