Skip to content

Go Data Types: Variables, Constants & Operations

Overview

We will disucss about GO's data types, variables, constants, and arithmetic operations.

Key Points

  • Go is statically typed with compile-time type checking
  • Rich set of built-in types for different use cases
  • Powerful variable declaration and initialization patterns
  • Constants provide compile-time guarantees
  • Type-safe arithmetic operations

Go's Type System

Go is a statically typed language, meaning every variable has a specific type known at compile time. This provides safety, performance, and clarity in your programs.

Type Categories

graph LR
    A[Go Data Types] --> B[Basic Types]
    A --> C[Composite Types]
    A --> D[Reference Types]
    A --> E[Interface Types]
    B --> B1[Numeric]
    B --> B2[String]
    B --> B3[Boolean]
    B1 --> B1a[Integers]
    B1 --> B1b[Floating Point]
    B1 --> B1c[Complex]
    C --> C1[Arrays]
    C --> C2[Structs]
    D --> D1[Slices]
    D --> D2[Maps]
    D --> D3[Channels]
    D --> D4[Pointers]
    style A fill:#1300,stroke:#333,stroke-width:2px,color:#999

Variables

Variables are named storage locations that hold values of a specific type. Go provides several ways to declare and initialize variables.

In Golang, we use var keyword to initialize a variable. The syntax is as follows:

1
var variable_name type = value // value is optional

Variable Declaration Methods

Declaration Patterns

explicit_types.go
package main

import "fmt"

func main() {
    var name string
    var age int
    var isStudent bool

    fmt.Printf("Name: %s, Age: %d, Student: %t\n", name, age, isStudent)
    // Output: Name: , Age: 0, Student: false
}

Zero Values

Variables declared without initialization get their type's zero value automatically.

var_init.go
package main

import "fmt"

func main() {
    var name string = "Alice"
    var age int = 25
    var isStudent bool = true

    fmt.Printf("Name: %s, Age: %d, Student: %t\n", name, age, isStudent)
    // Output: Name: Alice, Age: 25, Student: true
}
short_declaration.go
package main

import "fmt"

func main() {
    name := "Bob"        // Type inferred as string
    age := 30           // Type inferred as int
    isStudent := false  // Type inferred as bool

    fmt.Printf("Name: %s, Age: %d, Student: %t\n", name, age, isStudent)
    // Output: Name: Bob, Age: 30, Student: false
}

Most Common

Short declaration (:=) is the most commonly used method in Go. But please be minded that it is used when we know the value of the variable and it cannot be assigned as a global variable. If you do, you will get an error.

Multiple Variable Declaration

Declaring Multiple Variables

grouped_vars.go
package main

import "fmt"

func main() {
    var (
        name     string = "Charlie"
        age      int    = 35
        salary   float64 = 75000.50
        isActive bool   = true
    )

    fmt.Printf("Employee: %s, Age: %d, Salary: %.2f, Active: %t\n",
        name, age, salary, isActive)
    // Employee: Charlie, Age: 35, Salary: 75000.50, Active: true
}
multiple_assignment.go
package main

import "fmt"

func main() {
    name, age, city := "Diana", 28, "New York"
    var i int;
    var first, second, third bool

    fmt.Printf("Name: %s, Age: %d, City: %s\n", name, age, city)
    // Output: Name: Diana, Age: 28, City: New York
    fmt.Println(i, first, second, third)
    // Output: 0, false, false, false
}

Variable Naming Conventions

Go Naming Best Practices

  1. CamelCase: Use camelCase for variable names

    1
    2
    3
    firstName := "John"
    lastName := "Doe"
    phoneNumber := "123-456-7890"
    

  2. Descriptive Names: Choose meaningful names

    1
    2
    3
    4
    5
    6
    7
    // Good
    userAge := 25
    totalPrice := 99.99
    
    // Avoid
    a := 25
    tp := 99.99
    

  3. Short Names for Short Scope: Use short names for limited scope

    1
    2
    3
    for i := 0; i < 10; i++ {  // 'i' is fine for loop counter
        // ...
    }
    

  4. Exported vs Unexported: Capitalization determines visibility

    var PublicVariable = "visible outside package"
    var privateVariable = "only visible within package"
    
    Remember this for the next section when we discuss about standard library.

Basic Data Types Deep Dive

Integer Types

Go provides a rich set of integer types for different use cases and memory requirements.

Integer Type Reference

Type Size Range Use Case
int8 8 bits -128 to 127 Small signed values
int16 16 bits -32,768 to 32,767 Medium signed values
int32 32 bits -2.1B to 2.1B Large signed values
int64 64 bits -9.2E18 to 9.2E18 Very large signed values
uint8 8 bits 0 to 255 Small unsigned values
uint16 16 bits 0 to 65,535 Medium unsigned values
uint32 32 bits 0 to 4.3B Large unsigned values
uint64 64 bits 0 to 1.8E19 Very large unsigned values
int Platform 32 or 64 bits General purpose
uint Platform 32 or 64 bits General unsigned
uintptr Platform Pointer size Memory addresses

Although, go provides us with different data types for integers. We should be very conscious about the data types we use. For example, if we use int8 and try to assign a value of 128 to it, it will give us an error. Because the maximum value that can be stored in int8 is 127. In production we might always use inferred data's to the system. So we should use int for general purpose.

Integer Examples

integers.go
package main

import "fmt"

func main() {
    // Different integer types
    var smallNum int8 = 127
    var mediumNum int16 = 32767
    var largeNum int32 = 2147483647
    var veryLargeNum int64 = 9223372036854775807

    // Platform-dependent int
    var defaultInt int = 42

    // Unsigned integers
    var positiveNum uint8 = 255
    var bigPositive uint32 = 4294967295

    fmt.Printf("int8: %d\n", smallNum)
    fmt.Printf("int16: %d\n", mediumNum)
    fmt.Printf("int32: %d\n", largeNum)
    fmt.Printf("int64: %d\n", veryLargeNum)
    fmt.Printf("int: %d\n", defaultInt)
    fmt.Printf("uint8: %d\n", positiveNum)
    fmt.Printf("uint32: %d\n", bigPositive)
}
overflow.go
package main

import "fmt"

func main() {
    var maxInt8 int8 = 127
    fmt.Printf("Max int8: %d\n", maxInt8)

    // This would cause overflow (compile-time error)
    var overflow int8 = 128  // Error: constant 128 overflows int8

    // Runtime overflow wraps around
    maxInt8++
    fmt.Printf("After increment: %d\n", maxInt8) // -128
}

int can take both positive and negative numbers. So when we say var max int8, the varialbe max can hold value value from -128 to 127. So when we assign a positive value of 128 to a int8 it gives us overflow. But if we use var max uint8 we can have a number up to 0 - 255 as it can hold only positive numbers.

If you think how it's calculated, computer architecture primarily have a base 2 system (0s and 1s). So when we say we have a integer with 8 representation as bits, we can have a value of 256, that is 2^8.

Floating-Point Types

Go provides two floating-point types for decimal numbers.

Floating-Point Types

Type Size Precision Range
float32 32 bits ~7 decimal digits ±1.18E-38 to ±3.4E38
float64 64 bits ~15 decimal digits ±2.23E-308 to ±1.8E308
floats.go
package main

import "fmt"

func main() {
    var price float32 = 19.99
    var pi float64 = 3.141592653589793

    // Type inference defaults to float64
    temperature := 23.5

    fmt.Printf("Price: %.2f\n", price)
    fmt.Printf("Pi: %.15f\n", pi)
    fmt.Printf("Temperature: %.1f°C\n", temperature)

    // Scientific notation
    var scientific float64 = 1.23e-4
    fmt.Printf("Scientific: %e\n", scientific)
}
precision.go
package main

import "fmt"

func main() {
    var f32 float32 = 0.1
    var f64 float64 = 0.1

    // Floating-point precision differences
    fmt.Printf("float32: %.20f\n", f32)
    fmt.Printf("float64: %.20f\n", f64)
    // float32: 0.10000000149011611938
    // float64: 0.10000000000000000555
}

String Type

Strings in Go are immutable sequences of bytes, typically containing UTF-8 encoded text.

String Operations

strings.go
package main

import "fmt"

func main() {
    var greeting string = "Hello, World!"
    name := "Go"

    // String concatenation
    message := greeting + " Welcome to " + name + "!"

    // String length
    fmt.Printf("Message: %s\n", message)
    fmt.Printf("Length: %d bytes\n", len(message))

    // Raw strings (backticks)
    multiline := `This is a
    multi-line
    string`
    fmt.Println(multiline)
}
string_slicing.go
package main

import "fmt"

func main() {
    text := "Hello, Go!"

    // String indexing (returns byte values)
    fmt.Printf("First byte: %c (%d)\n", text[0], text[0])
    fmt.Printf("Last byte: %c (%d)\n", text[len(text)-1], text[len(text)-1])

    // String slicing
    fmt.Printf("Substring: %s\n", text[0:5])   // "Hello"
    fmt.Printf("From index 7: %s\n", text[7:]) // "Go!"
}

Boolean Type

The boolean type represents truth values: true or false.

Boolean Usage

booleans.go
package main

import "fmt"

func main() {
    var isActive bool = true
    var isComplete bool = false

    // Boolean operations
    fmt.Printf("Active: %t\n", isActive)
    fmt.Printf("Complete: %t\n", isComplete)

    // Logical operations
    fmt.Printf("AND: %t\n", isActive && isComplete)  // false
    fmt.Printf("OR: %t\n", isActive || isComplete)   // true
    fmt.Printf("NOT: %t\n", !isActive)              // false

    // Boolean in conditions
    if isActive {
        fmt.Println("System is running")
    }
}

At default the bool variable will be assigned a value false and an integer variable will be assigned a value of 0. String will be assigned a value "". As you have guessed, the complex, and float data types will be assigned to zero value based on their representation.

Key points to remember:

In Go, strings and integers are immutable. This means that once a string or integer is created, its content cannot be changed. Any operation that appears to modify a string or integer, such as concatenation or replacing characters, actually results in the creation of a new string or integer with the desired changes. The original string or integer remains unchanged in memory.

When you perform an operation that "changes" a string variable in Go, you are actually assigning a new string value to that variable name.

  1. Original string creation:

    1
    2
    3
    var1 := "apple"
    // In memory: "apple" is stored at a certain memory address (e.g., 0x1000)
    // var1 points to 0x1000
    
  2. Modification:

    When you appear to modify it, a new string value is created, and the var1 pointer is updated to point to this new memory location.

    1
    2
    3
    4
    var1 = var1 + " sauce"
    // A new string "apple sauce" is created at a *new* memory address (e.g., 0x1004).
    // The original "apple" at 0x1000 remains unchanged (until garbage collected).
    // var1 is updated to now point to 0x1004.
    

The original data "apple" itself was never mutated. It is still sitting at the original memory address, untouched. The variable var1 simply stopped pointing there and started pointing somewhere else entirely.

If you had another variable pointing to the original string, it would remain unchanged:

1
2
3
4
5
6
var1 := "apple"
var2 := var1 // var2 also points to "apple" at 0x1000

var1 = var1 + " sauce"
// var1 now points to the new "apple sauce" at 0x1004
// var2 *still* points to the original "apple" at 0x1000

This immutability ensures data safety, as the content that var2 sees is guaranteed not to change unexpectedly just because you reassigned var1.

Constants: Immutable Values

Constants are immutable values known at compile time. They provide guarantees and optimizations. They are hard coded during the compile time.

Constant Declaration

Constant Types

constants.go
package main

import "fmt"

func main() {
    const pi = 3.14159
    const greeting = "Hello"
    const maxUsers = 1000
    const isEnabled = true

    fmt.Printf("Pi: %.5f\n", pi)
    fmt.Printf("Greeting: %s\n", greeting)
    fmt.Printf("Max Users: %d\n", maxUsers)
    fmt.Printf("Enabled: %t\n", isEnabled)
}
typed_constants.go
package main

import "fmt"

func main() {
    const pi float64 = 3.14159265359
    const name string = "Go Language"
    const version int = 1
    const stable bool = true

    fmt.Printf("Language: %s v%d\n", name, version)
    fmt.Printf("Pi: %.10f\n", pi)
    fmt.Printf("Stable: %t\n", stable)
}
constant_groups.go
package main

import "fmt"

func main() {
    const (
        StatusPending   = "pending"
        StatusApproved  = "approved"
        StatusRejected  = "rejected"
        MaxRetries      = 3
        TimeoutSeconds  = 30
    )

    fmt.Printf("Status: %s\n", StatusPending)
    fmt.Printf("Max Retries: %d\n", MaxRetries)
    fmt.Printf("Timeout: %d seconds\n", TimeoutSeconds)
}

Enumerations with iota

Go doesn't have built-in enums, but you can create them using constants and iota.

Using iota for Enumerations

iota_basic.go
package main

import "fmt"

func main() {
    const (
        Sunday = iota    // 0
        Monday           // 1
        Tuesday          // 2
        Wednesday        // 3
        Thursday         // 4
        Friday           // 5
        Saturday         // 6
    )

    fmt.Printf("Sunday: %d\n", Sunday)
    fmt.Printf("Wednesday: %d\n", Wednesday)
    fmt.Printf("Saturday: %d\n", Saturday)
}
iota_advanced.go
package main

import "fmt"

func main() {
    const (
        _  = iota             // Skip 0
        KB = 1 << (10 * iota) // 1024
        MB                    // 1048576
        GB                    // 1073741824
        TB                    // 1099511627776
    )

    fmt.Printf("1 KB = %d bytes\n", KB)
    fmt.Printf("1 MB = %d bytes\n", MB)
    fmt.Printf("1 GB = %d bytes\n", GB)
}

So you might have asked this question => Why does const doesn't need data type declaration like variable declaration?

The answer is Go constants don't necessarily need explicit data types because of a core language feature: "they are untyped by default until they are used in a context that requires a specific type". This design provides flexibility and prevents many common type-mismatch bugs inherent in other statically typed languages like C and C++. These languages require explicit type declarations for constants, which can lead to errors if the type is not correctly specified.

Key reasons for untyped constants:

  • Type Inference: The compiler automatically infers a default type for a constant based on its literal value (e.g., 10 is an untyped integer, "hello" is an untyped string, 3.14 is an untyped floating-point number).

  • Flexibility and Interoperability: Untyped constants can seamlessly interact with variables of various compatible types without requiring explicit type conversions, which makes the code cleaner and more intuitive.

    1
    2
    3
    4
    5
    const Pi = 3.14159 // Pi is an untyped floating-point constant
    
    var f64 float64 = Pi // OK, Pi is implicitly used as a float64
    var f32 float32 = Pi // OK, Pi is implicitly used as a float32
    // A typed variable would cause an error here without a cast.
    
  • Arbitrary Precision: Untyped numeric constants have theoretically arbitrary precision (or at least 256 bits of precision) during compilation. This allows for complex constant-only arithmetic with high accuracy, only losing precision when finally assigned to a fixed-size variable type.

  • Compile-Time Evaluation: All constant expressions are evaluated entirely at compile time, meaning the final values are baked into the binary, resulting in potentially better runtime performance.

Arithmetic Operations

Go provides standard arithmetic operations with type safety and predictable behavior.

Basic Arithmetic Operators

Arithmetic Operations

arithmetic.go
package main

import "fmt"

func main() {
    a, b := 15, 4

    fmt.Printf("a = %d, b = %d\n", a, b)
    fmt.Printf("Addition: %d + %d = %d\n", a, b, a+b)
    fmt.Printf("Subtraction: %d - %d = %d\n", a, b, a-b)
    fmt.Printf("Multiplication: %d * %d = %d\n", a, b, a*b)
    fmt.Printf("Division: %d / %d = %d\n", a, b, a/b)
    fmt.Printf("Remainder: %d %% %d = %d\n", a, b, a%b)

    // Floating-point division
    fmt.Printf("Float Division: %.2f / %.2f = %.2f\n",
        float64(a), float64(b), float64(a)/float64(b))
}
assignment_ops.go
package main

import "fmt"

func main() {
    x := 10
    fmt.Printf("Initial x: %d\n", x)

    x += 5   // x = x + 5
    fmt.Printf("After x += 5: %d\n", x)

    x -= 3   // x = x - 3
    fmt.Printf("After x -= 3: %d\n", x)

    x *= 2   // x = x * 2
    fmt.Printf("After x *= 2: %d\n", x)

    x /= 4   // x = x / 4
    fmt.Printf("After x /= 4: %d\n", x)
}
increment.go
package main

import "fmt"

func main() {
    counter := 5
    fmt.Printf("Initial counter: %d\n", counter)

    counter++  // Increment by 1
    fmt.Printf("After counter++: %d\n", counter)

    counter--  // Decrement by 1
    fmt.Printf("After counter--: %d\n", counter)
}

Note: ++counter and --counter are not valid in Go. These are not expressions but statement. Also keep in mind that we cannot assign a variable to ++counter or --counter (like b := ++counter).

And if you need to use the value after the increment/decrement, you would do it in separate steps:

counter++
b := counter

Type Conversions

Go requires explicit type conversions between different numeric types. Go mandates explicit type conversions between different numeric types primarily to enhance type safety and prevent subtle bugs that can arise from implicit type coercion.

Type Conversion Examples

conversions.go
package main

import "fmt"

func main() {
    var i int = 42 // int
    var f float64 = float64(i)  // int to float64
    var u uint = uint(f)        // float64 to uint

    fmt.Printf("int: %d\n", i)
    fmt.Printf("float64: %.2f\n", f)
    fmt.Printf("uint: %d\n", u)

    // String conversions
    var str string = fmt.Sprintf("%d", i)
    fmt.Printf("string: %s\n", str)
}
safe_conversions.go
package main

import "fmt"

func main() {
    var bigNum int64 = 1000000

    // Safe conversion (value fits)
    var smallNum int32 = int32(bigNum)
    fmt.Printf("Safe conversion: %d\n", smallNum)

    // Potentially unsafe conversion
    var veryBig int64 = 9223372036854775807
    var truncated int32 = int32(veryBig)
    fmt.Printf("Truncated: %d\n", truncated) // May lose data
}

Advanced Type Concepts

Type Aliases and Definitions

If you have been a C/C++ developer, you might be familiar with the concept of typedef. In Go, we have similar but different concepts.

Custom Types

type_alias.go
package main

import "fmt"

func main() {
    type UserID = int64    // Type alias
    type Username = string // Type alias

    var id UserID = 12345
    var name Username = "alice"

    fmt.Printf("User ID: %d\n", id)
    fmt.Printf("Username: %s\n", name)

    // Can use interchangeably with underlying type
    var regularInt int64 = id
    fmt.Printf("Regular int: %d\n", regularInt)
}
type_definition.go
package main

import "fmt"

func main() {
    type Temperature float64  // New type
    type Distance int         // New type

    var temp Temperature = 23.5
    var dist Distance = 100

    fmt.Printf("Temperature: %.1f°C\n", temp)
    fmt.Printf("Distance: %d meters\n", dist)

    // Cannot mix with underlying type without conversion
    // var f float64 = temp  // Error!
    var f float64 = float64(temp)  // OK
    fmt.Printf("As float64: %.1f\n", f)
}

You might ask: Both type are float64. But why it won't work?

The answer is in Go, when you define a new type like type Temperature float64, it creates a distinct type that is not interchangeable with its underlying type (float64) without an explicit conversion. This is a safety feature to prevent accidental mixing of different semantic meanings, even if they have the same underlying representation.

Go does not allow implicit (automatic) conversion between named types and their underlying types. Even though Temperature is based on float64, Go treats Temperature as a separate type for clarity and safety.

Tip:

To verify the data type of a variable we can use the following method, %T.

var f float64 = float64(temp)
fmt.Println("%T", f) // return float64

Best Practices

Data Type Best Practices

  1. Choose Appropriate Types

    • Use int for general integer operations
    • Use specific sizes (int32, int64) when needed, not everywhere
    • Prefer float64 over float32 for precision
  2. Variable Naming

    • Use descriptive names for clarity
    • Follow Go naming conventions
    • Use short names for limited scope
  3. Constants Over Variables

    • Use constants for values that don't change
    • Group related constants together
    • Use iota for enumerations
  4. Type Safety

    • Explicit type conversions prevent bugs
    • Be careful with numeric overflow
    • Validate conversions when necessary

Common Pitfalls

  • Integer Overflow: Be aware of type limits
  • Float Precision: Floating-point arithmetic isn't exact
  • Type Mixing: Go doesn't allow implicit conversions
  • String Immutability: Strings can't be modified in place

Quick Reference

Key Takeaways

  1. Variables: Multiple declaration methods (:= most common)
  2. Types: Rich set of numeric, string, and boolean types
  3. Constants: Immutable values with compile-time guarantees
  4. Operations: Type-safe arithmetic with explicit conversions
  5. Best Practices: Choose appropriate types and follow conventions

Remember

"Go's type system is designed for clarity and safety. Embrace explicit conversions and descriptive naming for maintainable code."