Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Variables and Types

Go is a statically typed language, meaning variable types are known at compile time. However, its syntax is designed to be concise, often inferring types for you. Think of Go’s type system like a strict but helpful librarian: it will not let you put a novel on the science shelf (type safety), but it also will not make you fill out a form every time you check out a book (type inference). You get safety without ceremony.

Variable Declaration

There are three main ways to declare variables in Go.

1. var keyword (Explicit Type)

Use this when you want to declare a variable without initializing it immediately, or when you want to be explicit about the type.
var name string = "Gopher"
var age int = 10

2. var keyword (Type Inferred)

If you provide an initial value, you can omit the type.
var name = "Gopher" // inferred as string
var age = 10        // inferred as int

3. Short Declaration :=

Inside functions, you can use the := operator. This is the most common and idiomatic way to declare and initialize variables in Go.
func main() {
    name := "Gopher"  // Type inferred as string
    age := 10         // Type inferred as int
    isCool := true    // Type inferred as bool
}
Why := is Preferred: It’s concise and lets the compiler infer types, making code cleaner while maintaining type safety.
Restriction: The := syntax can only be used inside functions. At the package level (outside functions), you must use var.Common Gotcha: := declares a new variable. If you want to reassign an existing variable, use = instead:
name := "Alice"  // Declares new variable
name = "Bob"     // Reassigns existing variable
name := "Charlie" // ERROR: no new variable on left side of :=

Basic Types

Go has a rich set of built-in types.

Integers

  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64, uintptr
Note: int and uint are platform-dependent (32-bit on 32-bit systems, 64-bit on 64-bit systems). In 99% of cases, just use int. Only reach for specific-width types like int32 or int64 when you are dealing with serialization formats (protobuf, binary protocols), interacting with C code via cgo, or need to guarantee the bit width across platforms.

Floats

  • float32, float64
Note: float64 is the default for floating-point numbers.

Booleans

  • bool (true or false)

Strings

  • string (immutable sequence of bytes, UTF-8 encoded)

Complex Numbers

  • complex64, complex128

Zero Values

Variables declared without an initial value are given their zero value. This is a key safety feature: Go does not have uninitialized variables. Every variable always has a well-defined value.
TypeZero Value
int0
float0.0
boolfalse
string"" (empty string)
pointersnil
slicesnil
mapsnil
channelsnil
var i int
var s string
var p *int
fmt.Printf("%d %q %v\n", i, s, p) // Output: 0 "" <nil>
Why This Matters: Zero values make Go code more predictable and safer. You can rely on variables having sensible defaults:
var counter int  // Automatically 0, ready to use
counter++        // Now 1

var buffer bytes.Buffer  // Ready to use immediately, no initialization needed
buffer.WriteString("Hello")

Constants

Constants are declared using const. They can be character, string, boolean, or numeric values.
const Pi = 3.14
const World = "世界"
Constants cannot be declared using the := syntax.

Iota

iota is a special identifier used to create enumerated constants. It resets to 0 whenever const appears and increments by 1 for each line.
const (
    Red = iota   // 0
    Green        // 1
    Blue         // 2
)
Practical Use Cases:
// File permissions (powers of 2 for bitwise operations)
const (
    Read = 1 << iota  // 1 << 0 = 1
    Write             // 1 << 1 = 2
    Execute           // 1 << 2 = 4
)

// Skip values
const (
    _ = iota  // Skip 0
    KB = 1 << (10 * iota)  // 1024
    MB                      // 1048576
    GB                      // 1073741824
)

Type Conversion

Go requires explicit type conversion. There is no implicit casting (e.g., you cannot assign an int to a float64 variable without casting). This is a deliberate design decision — implicit conversions are a notorious source of subtle bugs in C and JavaScript.
var i int = 42
var f float64 = float64(i)  // Explicit: you see the conversion happening
var u uint = uint(f)         // Explicit: potential data loss is visible
If you try f = i, the compiler will throw an error.
Pitfall — Numeric Overflow: Go does not panic on integer overflow. Values silently wrap around. This can cause hard-to-find bugs:
var x uint8 = 255
x++                 // x is now 0, not 256 -- no error, no warning
If overflow matters for your use case, check bounds manually or use the math package constants (math.MaxInt64, math.MaxUint32, etc.) for guard checks.
Idiomatic Go: Prefer strconv over fmt.Sprintf for type conversions to/from strings. strconv.Itoa(42) is faster and more intention-revealing than fmt.Sprintf("%d", 42), and strconv.Atoi("42") returns a proper error for invalid input.

Interview Deep-Dive

Strong Answer:
  • Zero values mean every variable in Go is always initialized to a well-defined, usable value. An int is 0, a string is "", a bool is false, and a struct has all fields set to their respective zero values. This eliminates entire classes of “uninitialized variable” bugs that plague C and C++, and avoids the “billion dollar mistake” of null references in languages like Java.
  • Practically, this enables patterns like declaring a bytes.Buffer or a sync.Mutex and using them immediately without any constructor call. The zero value IS the valid initial state. This is a deliberate design principle: “make the zero value useful.”
  • Where it bites you: a nil map reads fine (returns zero values) but panics on write. A nil slice is safe to append to but a nil pointer to a struct will panic when you access a field. And the most subtle gotcha: a nil pointer inside an interface is NOT a nil interface. If you return a *MyError(nil) as an error interface, the caller’s err != nil check returns true because the interface has a type even though its underlying value is nil.
  • In production, the zero-value principle means you need to think carefully about whether “zero” is a valid state for your domain. For example, a UserID of 0 might look valid to Go but represent “no user” in your business logic. Some teams use pointer fields or custom types to distinguish “not set” from “set to zero.”
Follow-up: Why does Go require explicit type conversions between numeric types, and what real bug does this prevent?Go requires explicit conversions (like float64(myInt)) because implicit numeric conversions are a notorious source of silent bugs. In C, assigning a float to an int silently truncates. In JavaScript, adding a number to a string silently concatenates. Go’s explicit conversions force you to acknowledge potential data loss at the call site. For example, converting an int64 to int32 can overflow silently, but at least the conversion is visible in the code and can be caught during review. The trade-off is verbosity: Go code has more explicit casts than Python or JavaScript. But in production systems handling financial data or sensor readings, silent numeric truncation can cause real damage — an explicit int32(bigValue) makes the risk visible.
Strong Answer:
  • iota is a compile-time constant generator that resets to 0 at each const block and increments by 1 for each constant specification (each line in the block). It is Go’s replacement for C-style enums.
  • The simple case is sequential enums: Red = iota gives 0, 1, 2. But iota’s real power is that it can be used in expressions that are repeated implicitly. For example, you can define byte-size constants using 1 << (10 * iota): skipping the zero value with _, KB becomes 1024, MB becomes 1048576, GB becomes 1073741824.
  • A practical production example is bitwise permission flags: Read = 1 << iota gives you 1, 2, 4 which you can combine with OR operations: perms := Read | Write. This is exactly how Unix file permissions work, and it is how Go’s own os.FileMode constants are defined internally.
  • A more advanced pattern is using iota with a custom type and a String() method to create self-documenting enums that print as names instead of numbers. You define a type Status int, assign values with iota, then implement func (s Status) String() string using a slice or map lookup. This makes logging and debugging much clearer.
Follow-up: Go does not have a built-in enum type. If a function accepts a Status parameter typed as int, what prevents callers from passing an invalid value like 999?Nothing at the type level — this is a real limitation. Since Status is just an alias for int, any int value can be assigned to it. The Go compiler will not reject Status(999). The defense is runtime validation: either check the value at API boundaries with a func (s Status) IsValid() bool method, or use the exhaustive switch pattern where your switch on the status has no default case and the exhaustive linter flags any missing cases. Some teams also define a _maxStatus sentinel as the last iota value and validate that s >= 0 && s < _maxStatus. This is a deliberate trade-off in Go’s design — simplicity over exhaustive compile-time checking. Languages with real sum types (Rust, Haskell) solve this at the type level, but Go chose not to add that complexity.
Strong Answer:
  • Go does not panic on integer overflow. The value wraps around silently according to modular arithmetic. So uint8(255) + 1 becomes 0, not 256. For signed types, int8(127) + 1 becomes -128. There is no runtime error, no warning, nothing.
  • This is inherited from C’s behavior and exists because overflow checking on every arithmetic operation would add significant runtime overhead. Go chose performance over safety here, unlike Rust which panics on overflow in debug mode and wraps in release mode.
  • To defend against this in production: First, use int (which is 64-bit on modern systems) unless you have a specific reason to use a smaller type. The range of int64 is large enough that overflow is practically impossible for most business logic. Second, when working with user input or wire protocols that use fixed-width integers, add explicit bounds checks before arithmetic: if x > math.MaxUint8 - increment { return ErrOverflow }. Third, for critical financial calculations, consider using math/big for arbitrary-precision arithmetic. Fourth, the math package provides constants like math.MaxInt64, math.MaxUint32, etc., for guard checks.
  • The real-world scenario where this burns people: processing counters or metrics that monotonically increase. If you use a uint32 counter for HTTP requests and your service handles 100K requests per second, you overflow in about 12 hours.
Follow-up: Strings in Go are immutable sequences of bytes, not characters. What problems does this create when processing Unicode text?A Go string is a read-only slice of bytes, typically UTF-8 encoded but not guaranteed to be. len("hello") returns the byte count, not the character count. For ASCII this is the same, but for multi-byte UTF-8 characters like emoji or CJK text, len("...") gives a larger number than expected. Iterating with a byte index (s[i]) gives individual bytes, which can split a multi-byte character. The correct way to iterate over characters (runes) is for i, r := range s, which decodes UTF-8 on the fly. For character counting, use utf8.RuneCountInString(s). For string manipulation at the rune level, convert to []rune first, but be aware that this allocates a new slice. In production APIs that handle international text, always be explicit about whether your “length” means bytes or runes, and always validate that incoming strings are valid UTF-8 using utf8.Valid().