Friday, February 6, 2026

Variables and Data Types

    Profile Pic of Akash AmanAkash Aman

    Updated: February 2026

    Variables

    Concept: Go variables are statically typed. The := operator is for declaration + assignment, while = is only for assignment. var keyword can also be used for declaration or assignment.

    Why: Go enforces strict typing at compile time to prevent runtime errors like adding a string to an integer.

    go
    
    var foo string 					// declaration without initialization
    var foo string = "Go is awesome" // Declaration with initialization
    
    var name string = "Go" // Explicit type declaration + assignment
    age := 10              // Inferred type (Short declaration + assignment)
    age = 11               // Reassignment (must be same type)
    
    var foo, bar string = "Hello", "World" // Multiple declarations
    // OR
    var (								// Multiple declarations
    	foo string = "Hello" 
    	bar string  = "World"
    )
    
    • Shadowing (Crucial): Using := inside a block (like if) creates a new variable that hides the outer one.
    go
    x := 5
    if true {
    	x := 10 // New local 'x', doesn't change outer 'x'
    	fmt.Println(x) // Prints 10
    }
    fmt.Println(x) // Prints 5

    Constants

    Concept: Constants are values that cannot change. They can be Typed or Untyped.

    Why: Untyped constants have high precision and can be used flexibly with different types without explicit conversion.

    go
    const Pi = 3.14159 // Untyped (flexible)
    const TypedInt int32 = 42  // Strict
    const (
        A = iota // 0: iota auto-increments
        B        // 1
    )
    

    The Precision Justification

    • In many languages, if you define a constant like 3.14, the compiler immediately treats it as a float64. In Go, an untyped constant maintains extremely high precision (at least 256 bits) until it is actually assigned to a variable.
    go
    package main
    
    import "fmt"
    
    func main() {
    	// These are untyped constants
    	const huge = 1e100
    	const tiny = 1e-100
    
    	// The compiler does the math at 256-bit precision internally
    	const result = (huge + tiny) - huge 
    
    	// If this were 64-bit float math, 'tiny' would have been 
    	// rounded to 0 because it's too small compared to 'huge'.
    
    	var x float64 = result
    	fmt.Println(x) // Prints 1e-100
    }

    The Flexibility Justification (No Casting)

    • Go is famous for being "strict." You cannot add an int32 to an int64 without an explicit cast. However, untyped constants ignore this rule to make the code readable.
    go
    type MyFloat float64
    
    const PiConstant = 3.14   // Untyped
    var PiVariable = 3.14     	// Typed (inferred as float64) 
    
    func main() {
        var standard float64 = 2.0
        var custom MyFloat = 5.0
    
        // 1. Using the Untyped Constant (FLUID)
        fmt.Println(standard * PiConstant) // Works!
        fmt.Println(custom * PiConstant)   // Works! (Implicitly treated as MyFloat)
    
        // 2. Using the Typed Variable (STRICT)
        // fmt.Println(custom * PiVariable) // PANIC: Mismatched types (MyFloat vs float64)
    }

    Data Types

    In Go, a Value Type is a type where the variable directly contains its data. When you assign one to another, or pass it to a function, Go creates a complete, independent copy. If you change the copy, the original remains untouched. There are different value data type, i.e int, byte, rune, float, bool, string, array, struct.

    Don't worry, reference data types will be discussed individually in their respective chapters.

    Integer

    Go is very explicit about integers to ensure memory efficiency, especially for low-level or IoT programming.

    Signed Integers (int, int8, int16, int32, int64)

    Concept: Can store both positive and negative numbers.

    • Behavior: Uses "Two's Complement" logic. The most significant bit determines the sign.
    • Why: Go provides fixed sizes so you can match the data structure of a file format or network protocol exactly without wasting bits.

    Unsigned Integers (uint, uint8, uint16, uint32, uint64, uintptr)

    Concept: Can only store positive numbers (0 and up).

    • Special Alias: byte is an alias for uint8.
    • Why: Used when you need to ensure a value is never negative (like a memory address or a pixel color value). uintptr is specifically for storing the bit pattern of a memory pointer.

    In Go, an int16 and an int32 are completely different types. You cannot add them together without a manual conversion.

    Behavioral Reason: Go prioritizes Predictability. If the compiler automatically "upgraded" an int16 to an int32, you might accidentally consume more memory than intended or cause overflow issues when down-casting later.

    go
    var a int16 = 10
    var b int32 = 20
    
    // result := a + b // COMPILER ERROR: Mismatched types
    result := int32(a) + b // Valid
    
    
    var i int = 404                     // Platform dependent
    var i8 int8 = 127                   // -128 to 127
    var i16 int16 = 32767               // -2^15 to 2^15 - 1
    var i32 int32 = -2147483647         // -2^31 to 2^31 - 1
    var i64 int64 = 9223372036854775807 // -2^63 to 2^63 - 1
    
    var ui uint = 404                     // Platform dependent
    var ui8 uint8 = 255                   // 0 to 255
    var ui16 uint16 = 65535               // 0 to 2^16
    var ui32 uint32 = 2147483647          // 0 to 2^32
    var ui64 uint64 = 9223372036854775807 // 0 to 2^64
    var uiptr uintptr                     // Integer representation of a memory address

    Comparison Table

    TypeSize (Bits)Range / Use CaseZero Value
    bool8true, falsefalse
    int8 / uint88-128 to 127 / 0 to 2550
    int16 / uint1616Β±32,768 / 0 to 65,5350
    int32 / uint3232Β±2.1 Billion / 0 to 4.2 Billion0
    int64 / uint6464Massive ranges (for high precision)0
    int / uint32 or 64Platform dependent (Matches CPU architecture)0
    string128 (64-bit)Immutable pointer + length header""

    Boolean

    Concept: Represents a simple true or false, Unlike JavaScript or Python.

    • Zero Value: false.
    • Strictness: Go is one of the few languages that does not allow "truthiness." You cannot use an integer as a boolean. Go does not have "truthy" or "falsy" values. An integer 0 is not false, and an empty string is not false.

    Behavioral Reason: This prevents the "logic bugs" common in other languages where a value of 0 might accidentally trigger an error block.

    go
    var count int = 0
    // if count { ... } // COMPILER ERROR: Non-bool used as condition
    if count == 0 { ... } // Required

    String Types

    Concept: A read-only slice of bytes representing text.

    • Internal Structure: A "Header" containing a Pointer to a backing array and a Length.
    • Zero Value: "" (Empty string).

    Immuntability (The "Why")

    Behavior: You can reassign a string variable, but you cannot change its individual characters.

    Reason:

    • Safety: Multiple variables can point to the same underlying text in memory without fear that one will change it for everyone else.
    • Performance: Since the text is read-only, Go doesn't have to copy the actual text when you pass it to a functionβ€”it only copies the tiny 16-byte header.
    go
    s := "Hello"
    // s[0] = 'h' // Error: Strings are immutable
    s = "world"   // Valid: You just pointed the header to a new memory location
    

    byte and rune

    The byte (uint8)

    Concept: A byte is an alias for uint8. It represents 8 bits of data.

    Why: In Go, strings are internally just "slices of bytes." When you deal with ASCII characters (like 'A', 'B', '1'), a byte is enough to represent them.

    The rune (int32)

    Concept: A rune is an alias for int32.

    Why: Go uses UTF-8 encoding. Characters like emojis (πŸš€) or non-English letters (Γ±, δΈ–) require more than 8 bits. A rune is large enough (32 bits) to hold any Unicode character (a "Code Point").

    Behavioral Example: Why the difference matters

    • If you iterate over a string using a standard loop, you get bytes. If you use a range loop, Go automatically decodes the bytes into runes.
    go
    func main() {
        s := "GΓ³" // 'Γ³' takes 2 bytes in UTF-8
    
        // 1. Treating as Bytes (Value Type: uint8)
        fmt.Println("Bytes:")
        for i := 0; i < len(s); i++ {
            fmt.Printf("%x ", s[i]) // Prints hex for each byte
        }
        // Result: 47 c3 b3 (3 bytes)
    
        // 2. Treating as Runes (Value Type: int32)
        fmt.Println("\nRunes:")
        for _, r := range s {
            fmt.Printf("%c ", r) // Decodes the bytes into the character
        }
        // Result: G Γ³ (2 runes)
    }
    

    Why this behavior?

    • Efficiency: Storing everything as int32 (runes) would waste a massive amount of memory (4x more for standard English text). By using a byte-slice for strings and only converting to rune when needed, Go stays incredibly fast and memory-efficient.
    • Pass-by-Value: Since both are just numbers (uint8 or int32), they follow the standard Value Type rule: when you pass a byte or rune to a function, a copy is made, and the original is safe.

    Crux

    • byte = Use for raw data, ASCII, or binary files.
    • rune = Use when you need to handle text/characters accurately (Unicode).
    • Memory: Both are Value Types and are very cheap to pass around.

    In Go, Arrays are often the most misunderstood "Value Type" because they look like slices but behave completely differently in memory.

    Arrays

    Concept: Fixed Size as Identity

    An array is a collection of elements of a single type with a fixed length.

    • Key Rule: The size of the array is part of its type. [3]int and [5]int are distinct, incompatible types.
    • Why: Go allocates a contiguous block of memory for exactly that many elements. This makes indexing incredibly fast because the CPU knows exactly where each element sits.

    Behavioral Reason: Pure Pass-by-Value

    Unlike many other languages (like C or Java) where "arrays" are implicitly pointers, in Go, arrays are values.

    • The "Memory Tax": When you pass an array to a function, Go copies every single element into the function's scope.
    • Why this behavior? It ensures total isolation. No matter what a function does to the array it receives, the caller's original array is physically impossible to change.
    go
    func updateArray(data [3]int) {
        data[0] = 999 
        fmt.Println("Inside:", data) // [999, 2, 3]
    }
    
    func main() {
        original := [3]int{1, 2, 3}
        updateArray(original)
        fmt.Println("Outside:", original) // [1, 2, 3] (Unchanged!)
    }
    

    Initialization & Zero Value

    • Zero Value: An array is never "nil." Its zero value is an array of the same length where every element is set to its own zero value.
    • Efficiency Note: Because of the heavy "copying" cost, you will rarely see large arrays passed directly in Go. Instead, we use Slices (which point to arrays) or Pointers to Arrays.

    Crux

    • Definition: A fixed-length, contiguous block of memory.
    • Identity: Size + Type = Identity. You cannot pass [4]int into a function expecting [3]int.
    • Performance Gotcha: Passing an array of 1 million integers copies 8MB of data. If you don't want to copy, pass a Slice or a Pointer.

    Reference Type vs. Value Type

    Go categorizes types by how they are handled in memory:

    • Value Types: Store the actual data (int, float, bool, string, array, struct).
    • Reference Types: Store a pointer to the data (slice, map, channel, pointer, function).

    The Golden Rule: "Everything is a Copy"

    • In Go, when you pass a variable to a function, Go always makes a copy of whatever is in that variable’s "box."

    Value Types (Copying the House)

    • Types: int, byte, rune, float, bool, string, array, struct.
    • When you pass these, Go copies the entire data. If you have an array of 1 million integers, Go copies all 1 million integers into the function's memory.
    • Behavioral Reason: This ensures Isolation. The function cannot accidentally change the original data, making code easier to reason about in concurrent (multi-threaded) environments.
    go
    func updateArray(vals [3]int) {
        vals[0] = 999 // Only changes the local copy
    }
    
    func main() {
        nums := [3]int{1, 2, 3}
        updateArray(nums)
        fmt.Println(nums) // Prints [1, 2, 3] -> Original is safe
    }
    

    Reference Types (Copying the Key)

    • Types: slice, map, channel, pointer, interface.
    • These types are actually small headers (structs) that contain a pointer to an underlying data structure. When you pass a slice, Go copies the header (the pointer, the length, and the capacity), but not the underlying data (the backing array).
    • Behavioral Reason: This ensures Efficiency. You can pass a slice representing a 1GB file, and Go only copies about 24 bytes of "header" info, while still allowing the function to modify the actual file data.
    go
    func updateSlice(vals []int) {
        // 1. MUTATION: 
        // We are reaching through the copied pointer to the original backing array.
        vals[0] = 999
        fmt.Println(vals)    // Output: [999, 2, 3]
    
        // 2. REASSIGNMENT: 
        // We are giving the local 'vals' header a BRAND NEW pointer to a new array.
        // This breaks the link to the original slice in main().
        vals = []int{3, 2, 1} 
        fmt.Println(vals)    // Output: [3, 2, 1] (Local only)
    }
    
    func main() {
        nums := []int{1, 2, 3}
        updateSlice(nums)
    
        // Final Result:
        // The first change (mutation) stayed, but the second change (reassignment) didn't.
        fmt.Println(nums)    // Output: [999, 2, 3]
    }

    Crucial Concept: The "Pointer" Exception

    • If you want to modify a Value Type (like a struct or int) inside a function, you must pass a Pointer.
    • Why: You are changing the variable's "box" from containing the "Data" to containing the "Memory Address." Go copies the address, and the function follows that address back to the original house.

    Concept: String Headers (The Hybrid)

    Strings are technically Value Types, but they behave efficiently.

    • The Structure: A string header contains a pointer to the bytes and a length.
    • The Catch: Strings are immutable.
    • Behavior: When you pass a string, Go copies the header (cheap!), but because it's immutable, you can't change the underlying bytes. This gives you the speed of a reference type with the safety of a value type.
    go
    func updateString(s string) {
        // 1. MUTATION ATTEMPT:
        // s[0] = 'G' 
        // ^ COMPILER ERROR: Strings are immutable. You cannot reach through 
        // the pointer to change the underlying bytes.
    
        // 2. REASSIGNMENT:
        // We are giving the local 's' header a BRAND NEW pointer and length.
        // This points to a new memory location where "Go" is stored.
        s = "Go" 
        fmt.Println("Inside function:", s) // Output: Go
    }
    
    func main() {
        language := "Golang"
        updateString(language)
    
        // Final Result:
        // The original header in main() still points to the original 
        // "Golang" memory. Since the function couldn't mutate the 
        // underlying data, 'language' remains unchanged.
        fmt.Println("Outside function:", language) // Output: Golang
    }

    Summary

    Type CategoryWhat is Copied?Impact of MutationImpact of ReassignmentMemory Cost
    Value (int, byte, rune)Entire DataOriginal is safeLocal onlyVery Low (1–4 bytes)
    Value (Array)The entire data setOriginal is safeHigh (increases with size)
    Reference (slice, map)Header (Pointer)Original changesLocal onlyLow (fixed)
    Pointer (*int, *struct)Memory AddressOriginal changesOriginal changesLow (8 bytes)
    StringHeader (Pointer)Impossible (Read-only)Local onlyLow (fixed)
    append behaviour for slice depends up on the capacity, we will this in detail in slice chapter.

    Zero Value

    Concept: Variables declared without a value automatically get a "Zero Value." Why: This prevents "garbage memory" bugs common in C.

    • int: 0
    • bool: false
    • string: "" (empty string)
    • pointers/slices/maps: nil
    go
    var i int
    var f float64
    var b bool
    var s string
    
    fmt.Printf("%v %v %v %q\n", i, f, b, s) // 0 0 false ""

    Initialization

    In Go, Initialization is the process of preparing a variable for use. Understanding the difference between var, new, and make is critical because using the wrong one can lead to the most famous Go error: panic: runtime error: invalid memory address or nil pointer dereference.

    Value Types (Automatic Initialization)

    Value types (int, bool, struct, array) are never "empty." When you declare them, Go automatically allocates memory and fills it with the Zero Value.

    • Behavior: They are ready to use immediately upon declaration.
    • Mechanism: Memory is usually allocated on the Stack.
    go
    var i int          // Ready to use (0)
    var s MyStruct     // Ready to use (all fields at zero value)
    

    Reference Types (The nil Problem)

    Reference types (slice, map, chan) store a header that points to an underlying data structure.

    • Behavior: When declared with var, the header's pointer is nil.
    • The Trap: You cannot store data in a nil map or channel; it will cause a crash.
    go
    var m map[string]int 
    // m["key"] = 10 // PANIC: assignment to entry in nil map

    new() vs make()

    To handle initialization, Go provides two built-in functions that serve completely different purposes.

    The new Function

    • What it does: Allocates memory for a type and returns a pointer to it. It sets the memory to the zero value.
    • Result: Returns *T (a pointer).
    • When to use: Use it when you need a pointer to a value type (like a struct) to share it across functions.
    go
    p := new(int)   // p is of type *int, points to a value of 0
    type User struct { Name string }
    u := new(User)  // Returns *User; equivalent to &User{}
    

    The make Function

    • What it does: Initializes Reference Types only (slice, map, channel). Unlike new, it creates the internal data structure (the backing array or hash table) so the header is no longer nil. make also allows you to pre-allocate memory.
    • Result: Returns T (the actual type, not a pointer).
    • When to use: Always use make for maps and channels. Use it for slices when you know the initial size or capacity.
    go
    s := make([]int, 5, 10) // Creates a slice header AND a backing array
    m := make(map[string]int) // Creates the hash table so you can add keys
    fmt.Println(s) // Output: [0 0 0]

    The Big Difference: var vs make

    This is the most important "behavioral reason" to include in your Initialization section:

    Featurevar s []int (Declaration)s := make([]int, 3) (Initialization)
    The HeaderExists, but Pointer is nilExists and points to an array
    The MemoryNo backing array allocatedBacking array allocated
    The ElementsNo elements existElements initialized to Zero Value

    Examples

    Slices: Zeroing the Backing Array

    When you use make for a slice, Go does two things:

    1. It creates the Header (Pointer, Length, Capacity).
    2. It allocates a Backing Array and fills every slot with the Zero Value of the element type.
    go
    s := make([]int, 3) 
    fmt.Println(s) // Output: [0 0 0] 
    

    Why? Go ensures memory safety. If it didn't zero out that array, your slice would contain "garbage data" left over from whatever was in that memory previously.

    Maps and Channels: Preparing the "Room"

    For maps and channels, make initializes the internal data structure so it is ready to receive data.

    • Maps: It creates the hash table buckets. The keys and values don't exist yet, so there are no "zero values" to see until you actually start adding keys.
    • Channels: It creates the buffer. If it's a buffered channel, the slots in that buffer are essentially "empty," but memory is reserved.

    Comparison

    Featurenew(T)make(T, args)
    Applicable TypesAll types (int, struct, etc.)Only Slice, Map, Channel
    Return TypePointer (*T)Value (T)
    Memory StateZeroed (but pointers inside may be nil)Fully Initialized (ready to use)
    GoalGet a pointer to a zero-valueInitialize internal data structures

    Behavioral Reasoning

    • Indirection vs. Initialization: new is a generic allocator. It doesn't know anything about the "internals" of a type; it just gives you a clean slot of memory.
    • Complex Internals: Slices, Maps, and Channels are complex. They aren't just memory slots; they require a "backing" structure (like an array for a slice or a hash table for a map). make is a specialized constructor that sets up these hidden parts so you don't have to manage them manually.

    "Does make assign zero values? Yes. For slices, it populates the length of the slice with the type's zero value. For maps and channels, it initializes the internal machinery so they are no longer nil and are ready for use."

    • The Golden Rule: If you want a Pointer to an object β†’ use new or &Type{}. If you want a Slice, Map, or Channel β†’ use make.

    Type Conversion

    Concept: Moving a value from one type to another. In Go, this must be done explicitly.

    Why: Go does not have "Implicit Conversion" (where the compiler automatically changes an int to a float). This prevents hidden precision loss or bugs that occur in languages like C++ or JavaScript.

    go
    func main() {
        var i int = 42
        var f float64 = float64(i) // Explicit conversion
        var u uint = uint(f)       // Explicit conversion
        
        // fmt.Println(i + f) 
        // ^ COMPILER ERROR: invalid operation (mismatched types int and float64)
        
        fmt.Println(float64(i) + f) // Success: Both are now float64
    }
    

    Behavioral Reason: By forcing you to write the conversion, Go ensures you are aware that you might be losing data (like converting a float64 to an int which chops off the decimals).

    Defined Types

    Concept: Creating a brand new type based on an existing one.

    Why: This allows you to add methods to a type and creates "Domain Logic" safety. Even if the underlying data is the same (e.g., both are int), a Duration is not the same as a Temperature.

    go
    type Celsius float64
    type Fahrenheit float64
    
    func main() {
        var c Celsius = 100
        // var f Fahrenheit = c 
        // ^ COMPILER ERROR: cannot use c (type Celsius) as type Fahrenheit
        
        var f Fahrenheit = Fahrenheit(c) // Success: Explicit conversion required
        fmt.Println(f)	// 100
    }

    Behavioral Reason: Go treats Celsius and Fahrenheit as totally different types. This prevents you from accidentally adding degrees Celsius to degrees Fahrenheit in your code, which would be a logical disaster.

    Alias Types

    Concept: Creating an alternative name for an existing type. Use the = symbol.

    Why: Aliases are used mostly for Refactoring. If you move a type from one package to another, you can leave an alias behind so old code doesn't break.

    go
    type Kilometers = float64 // Alias
    
    func main() {
        var distance float64 = 10.5
        var k Kilometers = distance // Success: No conversion needed!
        
        fmt.Printf("Type: %T\n", k) // Prints: float64
    }
    

    Behavioral Reason: An alias is not a new type. It is just a second name for the same thing. The compiler treats Kilometers and float64 as identical.

    Summary

    FeatureDefined Type (type T1 T2)Alias Type (type T1 = T2)
    Is it a new type?Yes (Distinct identity)No (Just a nickname)
    Implicit Conversion?No (Must cast manually)Yes (They are the same)
    Can add methods?YesNo (Only on the original type)
    Use CaseDomain logic, Type safetyCode refactoring, Package migration

    Β© 2026 Akash Aman | All rights reserved