Data Types #

Choosing the right data type isn’t just a formality to make code compile. The choice between int32 and int64 determines whether your program overflows when counting large database IDs. The choice between float64 and integers determines whether your financial calculations are accurate or hide rounding errors. The choice between string and []byte determines how efficiently you manipulate binary data. Go is a statically typed language — every value has a type known at compile time — and its type system is designed so wrong choices are easily caught by the compiler.

Data Type Categories in Go #

Go divides data types into several main categories. This grouping can be visualized in the following diagram:

flowchart TD
    DataType["Go Data Types"] --> Basic["Basic Types"]
    DataType --> Composite["Composite Types"]
    DataType --> Ref["Reference & Others"]

    Basic --> Num["Numeric"]
    Basic --> Bool["Boolean (bool)"]
    Basic --> Str["String (string)"]

    Num --> Int["Integer (int, int8, uint, uint8, uintptr, etc.)"]
    Num --> Float["Float (float32, float64)"]
    Num --> Complex["Complex (complex64, complex128)"]

    Composite --> Arr["Array: [N]T"]
    Composite --> Sli["Slice: []T"]
    Composite --> Mp["Map: map[K]V"]
    Composite --> St["Struct: struct { ... }"]

    Ref --> Ptr["Pointer: *T"]
    Ref --> Fn["Function: func(...)"]
    Ref --> Intf["Interface: interface { ... }"]
    Ref --> Ch["Channel: chan T"]

This article focuses on basic types and pointers. Composite types (array, slice, map, struct) each have their own article because of their complexity.


Integers #

Go provides integers with explicit bit sizes. This differs from many other languages that only have a single int type — in Go you can (and often need to) choose the right size.

Signed Integers — Can Be Negative #

var i8  int8  = 127              // -128 to 127
var i16 int16 = 32767            // -32,768 to 32,767
var i32 int32 = 2147483647       // about ±2.1 billion
var i64 int64 = 9223372036854775807  // about ±9.2 quintillion

// int — its size follows the platform: 32-bit on 32-bit systems, 64-bit on 64-bit
// This is the most commonly used for general purposes
var i int = 100

Unsigned Integers — Non-Negative Only #

var u8   uint8  = 255                    // 0 to 255
var u16  uint16 = 65535                  // 0 to 65,535
var u32  uint32 = 4294967295             // 0 to about 4.3 billion
var u64  uint64 = 18446744073709551615   // 0 to about 18.4 quintillion

// uint — follows the platform like int
var u uint = 100

// uintptr — stores a memory address, used in low-level code
var ptr uintptr

Guide to Choosing Integer Types #

This is a decision that often confuses beginners. Here’s a practical guide:

USE int (default) for:
  ✓ Loop indexes: for i := 0; i < n; i++
  ✓ Counters and general quantities
  ✓ Values that don't need a specific size

USE int64 for:
  ✓ Database IDs (especially auto-increment ones that can get very large)
  ✓ Unix timestamps (seconds since 1970): time.Now().Unix()
  ✓ File sizes in bytes (large files can exceed int32)
  ✓ Calculations that can produce very large numbers

USE int32 for:
  ✓ Interoperability with C libraries or network protocols using int32
  ✓ Protocol Buffers (protobuf) — the int32 type in .proto files

USE uint8 for:
  ✓ Byte data (alias: byte)
  ✓ RGB pixel color values (0-255)
  ✓ Raw binary data

USE uint16 for:
  ✓ Port numbers (0-65535)
  ✓ BMP Unicode characters

AVOID unsigned for general business logic:
  ✗ Underflow happens easily: uint(0) - 1 = 18446744073709551615 (not -1!)

Integer Overflow #

Overflow in Go doesn’t cause a panic — it “wraps around” silently:

var x int8 = 127
x++
fmt.Println(x)  // Output: -128  (not 128!)

var u uint8 = 0
u--
fmt.Println(u)  // Output: 255  (underflow, not -1!)
Integer overflow in Go doesn’t cause a runtime error — the value just “wraps” back to the other end of the range. This can become a bug that’s very hard to detect. For operations at risk of overflow, always validate the value range before the operation or use a safe math library.

Floats #

Go provides two floating-point types following the IEEE 754 standard:

var f32 float32 = 3.14           // ~7 decimal digits of precision
var f64 float64 = 3.14159265358979  // ~15-17 decimal digits of precision

// The default for decimal literals is float64
ratio := 1.5      // type: float64
pi    := 3.14159  // type: float64

float32 vs float64 — When to Use Which? #

In practice, almost always use float64. The reasons: every math function in the math package works with float64, and float32 precision (7 digits) is often insufficient for lengthy calculations.

import "math"

// All math functions use float64
fmt.Println(math.Sqrt(2))      // 1.4142135623730951
fmt.Println(math.Sin(math.Pi)) // 1.2246467991473532e-16 (close to 0)
fmt.Println(math.Abs(-3.14))   // 3.14

// float32 only makes sense for:
// - 3D graphics (OpenGL, game engines) — GPUs are natively float32
// - Very large numeric datasets that need memory savings
// - Interop with C libraries using float

Floating-Point Precision Issues #

This isn’t a bug in Go — it’s a fundamental property of floating-point representation in every language:

fmt.Println(0.1 + 0.2)   // Output: 0.30000000000000004
fmt.Println(0.1 + 0.2 == 0.3)  // Output: false !

// Float comparisons must use an epsilon (tolerance)
const epsilon = 1e-9
a := 0.1 + 0.2
b := 0.3
fmt.Println(math.Abs(a-b) < epsilon)  // Output: true

Don’t Use Floats for Money #

This is a very common mistake that can cause real losses:

// ANTI-PATTERN: financial calculations with floats
price := 9999.99
tax := price * 0.11  // 11%
total := price + tax
fmt.Printf("Total: %.2f\n", total)  // looks OK in the output
// But: price*0.11 = 1099.9989000000001, not exactly 1099.9989

// CORRECT: use integers in the smallest unit (cents or rupiah)
// Store all prices in the smallest unit (integer)
priceRupiah := 999999  // Rp 9.999,99 stored as 999999 (in cents)
taxSen := priceRupiah * 11 / 100  // integer division
totalSen := priceRupiah + taxSen
fmt.Printf("Total: Rp %.2f\n", float64(totalSen)/100)

For production financial applications, consider libraries like github.com/shopspring/decimal which provide a Decimal type with arbitrary precision.

Special Float Values #

import "math"

posInf := math.Inf(1)   // +Infinity
negInf := math.Inf(-1)  // -Infinity
nan    := math.NaN()    // Not a Number

fmt.Println(math.IsInf(posInf, 1))   // true
fmt.Println(math.IsNaN(nan))          // true
fmt.Println(nan == nan)               // false! NaN is not equal to itself

Complex Numbers #

Complex number types are available directly in Go without any additional library:

var z1 complex64  = 3 + 4i
var z2 complex128 = 1.5 + 2.5i

// Or use the complex() function
z3 := complex(3.0, 4.0)  // 3+4i

// Access the real and imaginary parts
fmt.Println(real(z3))  // 3
fmt.Println(imag(z3))  // 4

// Arithmetic operations
sum := z1 + complex64(z2)
fmt.Println(sum)  // (4.5+6.5i)

Complex types are rarely used in everyday application development — they’re most relevant for scientific computing, digital signal processing (DSP), or computer graphics involving Fourier transforms.


Booleans #

bool has only two values: true and false. Its zero value is false.

var active bool = true
var inactive bool   // zero value: false

// Results of comparison operations are always bool
x := 42
fmt.Println(x > 10)    // true
fmt.Println(x == 10)   // false
fmt.Println(x != 42)   // false
fmt.Println(x >= 42)   // true

Logical Operators and Short-Circuit Evaluation #

a, b := true, false

fmt.Println(a && b)   // false — AND: both must be true
fmt.Println(a || b)   // true  — OR: one of them being true is enough
fmt.Println(!a)       // false — NOT: negation

// Short-circuit evaluation — important for safety
// If the left side of && is already false, the right side is NOT evaluated
var p *int = nil
if p != nil && *p > 0 {  // safe: *p is not evaluated if p == nil
    fmt.Println("positive")
}

// If the left side of || is already true, the right side is NOT evaluated
func isAdmin(user *User) bool {
    return user != nil && user.Role == RoleAdmin
}
Short-circuit evaluation isn’t just a performance optimization — it’s an important safety pattern in Go. Always put nil checks or “guard” conditions on the left side of &&, and expensive conditions on the right side, which are only evaluated when needed.

Strings #

A string in Go is an immutable sequence of bytes — a byte order that can’t be changed once created. This differs from many other languages where strings can be modified in place.

s := "Hello, Go!"

// len() returns the number of BYTES, not characters
fmt.Println(len(s))   // 10

// Element access using an index — yields a byte (uint8), not a character
fmt.Println(s[0])         // 72 (byte value for 'H')
fmt.Println(string(s[0])) // "H"

// Strings can't be modified directly
// s[0] = 'h'  // ← compile error: cannot assign to s[0]

// To modify, convert to []byte, modify, convert back
b := []byte(s)
b[0] = 'h'
s2 := string(b)
fmt.Println(s2)  // "hello, Go!"

Raw String Literals #

Besides regular double-quoted strings, Go supports raw string literals using backticks. Raw strings don’t process any escape sequences:

// Regular string — escape sequences are processed
s1 := "first line\nsecond line\ttab"

// Raw string literal — shown exactly as-is, including newlines
s2 := `first line\nsecond line
    literal tab`

// Very useful for:
// - Multi-line SQL queries
query := `
    SELECT u.id, u.name, u.email
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE u.active = true
    ORDER BY u.created_at DESC
    LIMIT 10
`

// - Regex patterns (no need to escape backslashes)
pattern := `^\d{4}-\d{2}-\d{2}$`   // raw: no escaping
// vs
pattern2 := "^\\d{4}-\\d{2}-\\d{2}$"  // must escape backslashes

// - JSON in tests
jsonData := `{"name": "Budi", "age": 28}`

String Operations with the strings Package #

import "strings"

s := "  Hello, Go!  "

// Basic manipulation
fmt.Println(strings.ToUpper(s))          // "  HELLO, GO!  "
fmt.Println(strings.ToLower(s))          // "  hello, go!  "
fmt.Println(strings.TrimSpace(s))        // "Hello, Go!"
fmt.Println(strings.Trim(s, " "))        // "Hello, Go!"

// Searching
fmt.Println(strings.Contains(s, "Go"))   // true
fmt.Println(strings.HasPrefix(s, "  H")) // true
fmt.Println(strings.HasSuffix(s, "!  ")) // true
fmt.Println(strings.Index(s, "Go"))      // 9

// Transformation
fmt.Println(strings.Replace(s, "Go", "Golang", 1)) // "  Hello, Golang!  "
fmt.Println(strings.ReplaceAll(s, " ", "_"))        // "__Hello,_Go!__"

// Split and Join
parts := strings.Split("a,b,c,d", ",")  // ["a" "b" "c" "d"]
joined := strings.Join(parts, " | ")    // "a | b | c | d"
fields := strings.Fields("  foo bar  baz  ")  // ["foo" "bar" "baz"] (split on whitespace)

// Converting to/from numbers — use strconv
import "strconv"
n, err := strconv.Atoi("123")        // string → int
s3 := strconv.Itoa(456)              // int → string
f, err := strconv.ParseFloat("3.14", 64)  // string → float64
b2, err := strconv.ParseBool("true")      // string → bool

String Builder — Efficient for Concatenating Many Strings #

import "strings"

// ANTI-PATTERN: concatenation with + in a loop — very inefficient
// Every += allocates a new string in memory
result := ""
for i := 0; i < 1000; i++ {
    result += fmt.Sprintf("item %d, ", i)  // O(n²) memory allocations!
}

// CORRECT: use strings.Builder
var sb strings.Builder
for i := 0; i < 1000; i++ {
    fmt.Fprintf(&sb, "item %d, ", i)  // only one allocation at the end
}
result2 := sb.String()

Byte and Rune — Important Type Aliases #

byte — An Alias for uint8 #

byte is an alias name for uint8. It’s used when the context clearly indicates the value is binary data or an ASCII character:

var b byte = 'A'
fmt.Println(b)         // 65 (ASCII value)
fmt.Println(string(b)) // "A"

// Converting a string to []byte for binary manipulation
data := []byte("Hello")
data[0] = 'h'
fmt.Println(string(data))  // "hello"

rune — An Alias for int32 #

rune is an alias name for int32. It represents a single Unicode code point — one Unicode character. This matters because Unicode characters can require more than one byte:

var r rune = '界'
fmt.Println(r)         // 30028 (Unicode code point U+754C)
fmt.Println(string(r)) // "界"

// Unicode strings — the difference between len and character count
s := "Hello, 世界"
fmt.Println(len(s))           // 13 (bytes, not characters!)
fmt.Println(len([]rune(s)))   // 9  (Unicode characters)

// Iterating with range — per RUNE, not per byte
for i, r := range s {
    fmt.Printf("index %d: %c (U+%04X)\n", i, r, r)
}
// index 0: H (U+0048)
// index 1: e (U+0065)
// ...
// index 7: 世 (U+4E16)  ← index jumps to 10 next because 世 is 3 bytes
// index 10: 界 (U+754C)

// Correctly accessing the N-th character (not s[N])
runes := []rune(s)
fmt.Println(string(runes[7]))  // "世" — the 8th character (index 7)

Pointers #

A pointer stores the memory address of a variable. Go has pointers, but they’re much safer than in C because there’s no pointer arithmetic — you can’t do ptr + 1 to move to the next address.

func main() {
    x := 42

    p := &x           // p is a *int, storing x's address
    fmt.Println(p)    // Output: 0xc0000b4010 (example address)
    fmt.Println(*p)   // Output: 42  — dereference: read the value at that address

    *p = 100          // write a new value to the address p holds
    fmt.Println(x)    // Output: 100  — x changed!
}

Why Pointers Are Needed #

Without pointers, all values in Go are passed by value — functions receive a copy. Changes inside the function don’t affect the original variable:

// ANTI-PATTERN: want to modify the original value but don't use a pointer
func doubleValue(n int) {
    n = n * 2  // only modifies the local COPY
}

func main() {
    x := 5
    doubleValue(x)
    fmt.Println(x)  // Output: 5 — NOT changed!
}

// CORRECT: use a pointer to modify the original value
func doubleValuePtr(n *int) {
    *n = *n * 2  // modifies the value at the given address
}

func main() {
    x := 5
    doubleValuePtr(&x)  // send x's address
    fmt.Println(x)      // Output: 10 — changed!
}

Pointers to Structs — The Most Common Case #

Pointers are most often used with structs, especially as return types and function parameters to avoid copying large structs:

type User struct {
    ID    int
    Name  string
    Email string
    // imagine 20 more fields...
}

// Returning *User is more efficient than User for large structs
func NewUser(name, email string) *User {
    return &User{
        Name:  name,
        Email: email,
    }
}

// Pointer receiver — methods can modify the struct
func (u *User) UpdateEmail(email string) {
    u.Email = email  // Go auto-dereferences: no need for (*u).Email
}

func main() {
    user := NewUser("Budi", "[email protected]")
    user.UpdateEmail("[email protected]")
    fmt.Println(user.Email)  // [email protected]
}

Nil Pointers — A Common Panic Source #

var p *int  // a pointer's zero value is nil

// ANTI-PATTERN: dereferencing a nil pointer → PANIC
fmt.Println(*p)  // panic: runtime error: invalid memory address or nil pointer dereference

// CORRECT: always check nil before dereferencing
if p != nil {
    fmt.Println(*p)
} else {
    fmt.Println("pointer is nil")
}

The new() Function #

new(T) allocates memory for type T, initializes it with the zero value, and returns a pointer to that memory:

p := new(int)          // a *int pointing to an int with value 0
fmt.Println(*p)        // 0

s := new(string)       // a *string pointing to an empty string ""
fmt.Println(*s)        // ""

// Equivalent to:
n := 0
p2 := &n

In practice, new() is rarely used for basic types — using &Struct{} for structs is more common.


Type Conversion — Always Explicit #

Go never converts types implicitly. Every conversion must be written explicitly. This prevents hidden bugs common in C or JavaScript.

var i int = 42
var f float64 = float64(i)   // int → float64
var u uint = uint(i)          // int → uint
var i32 int32 = int32(i)      // int → int32

// Converting between strings and numbers
import "strconv"

// String → int
n, err := strconv.Atoi("123")
if err != nil {
    fmt.Println("not a valid number")
}

// Int → string
s := strconv.Itoa(456)

// String → float64
f2, err := strconv.ParseFloat("3.14", 64)

// Float → string with formatting
s2 := strconv.FormatFloat(3.14159, 'f', 2, 64)  // "3.14"

Gotcha: Converting int to string #

This is a very common trap:

n := 65
fmt.Println(string(n))       // Output: "A"  ← ASCII character 65, NOT "65"!
fmt.Println(strconv.Itoa(n)) // Output: "65" ← this is usually what you want
fmt.Println(fmt.Sprintf("%d", n)) // Output: "65"

string(65) converts the integer to the Unicode character with code point 65 (the letter ‘A’), not to the string “65”. Use strconv.Itoa() or fmt.Sprintf("%d", n) to convert a number to its text representation.


Type Definition vs Type Alias #

Go supports two ways to create a “new name” for an existing type, and they have very different semantics.

Type Definition — A New Type #

// Type definition: Celsius is a NEW type, different from float64
type Celsius float64
type Fahrenheit float64

var c Celsius = 100
var f Fahrenheit = 212

// Can't mix them directly even though both are based on float64
// c = f  // ← compile error: cannot use f (type Fahrenheit) as type Celsius

// Must convert explicitly
c2 := Celsius(f)  // converts the value, but conceptually meaningless

// Type definitions allow methods
func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func main() {
    boiling := Celsius(100)
    fmt.Println(boiling.ToFahrenheit())  // 212
}

Type Alias — Another Name, the Same Type #

// Type alias: byte is ANOTHER name for uint8, both are identical
type byte = uint8   // definition in the standard library
type rune = int32   // definition in the standard library

// Aliases are interchangeable without conversion
var b byte = 65
var u uint8 = b  // ✓ no conversion needed — they're the exact same type

Aliases are mainly used for gradual refactoring (moving a type from one package to another without breaking changes) and for more descriptive names like byte and rune.


Complete Example Program #

The following program uses various data types in a store inventory system context:

package main

import (
    "fmt"
    "math"
    "strings"
)

// Type definitions for semantic clarity
type ProductID int64
type StockUnit uint32
type PriceRupiah int64   // price in rupiah units (integer, not float!)

type Category byte

const (
    CategoryElectronics Category = iota + 1
    CategoryClothing
    CategoryFood
    CategoryBooks
)

func (c Category) String() string {
    switch c {
    case CategoryElectronics:
        return "Electronics"
    case CategoryClothing:
        return "Clothing"
    case CategoryFood:
        return "Food"
    case CategoryBooks:
        return "Books"
    default:
        return fmt.Sprintf("Category(%d)", c)
    }
}

type Product struct {
    ID       ProductID
    Name     string
    Category Category
    Price    PriceRupiah
    Stock    StockUnit
    Active   bool
}

// Pointer receiver — modifies stock
func (p *Product) AddStock(qty StockUnit) {
    p.Stock += qty
}

func (p *Product) Sell(qty StockUnit) error {
    if p.Stock < qty {
        return fmt.Errorf("insufficient stock: %d available, %d requested", p.Stock, qty)
    }
    p.Stock -= qty
    return nil
}

// Value receiver — read-only
func (p Product) FormattedPrice() string {
    // Format the price with dots as thousands separators
    price := int64(p.Price)
    s := fmt.Sprintf("%d", price)
    // Add a dot every 3 digits from the back
    var result strings.Builder
    for i, c := range s {
        if i > 0 && (len(s)-i)%3 == 0 {
            result.WriteByte('.')
        }
        result.WriteRune(c)
    }
    return "Rp " + result.String()
}

func (p Product) IsLowStock() bool {
    return p.Stock < 10 && p.Active
}

func calculateDiscount(price PriceRupiah, discountPct float64) PriceRupiah {
    // Discount calculation stays accurate because the base is an integer
    discount := PriceRupiah(math.Round(float64(price) * discountPct / 100))
    return price - discount
}

func main() {
    products := []Product{
        {
            ID:       1,
            Name:     "UltraBook Pro Laptop",
            Category: CategoryElectronics,
            Price:    15000000,
            Stock:    25,
            Active:   true,
        },
        {
            ID:       2,
            Name:     "Premium Plain T-Shirt",
            Category: CategoryClothing,
            Price:    85000,
            Stock:    5,
            Active:   true,
        },
        {
            ID:       3,
            Name:     "Go Programming Language",
            Category: CategoryBooks,
            Price:    320000,
            Stock:    50,
            Active:   true,
        },
    }

    fmt.Println("=== STORE INVENTORY ===\n")

    for i := range products {
        p := &products[i]  // pointer so Sell() can modify

        fmt.Printf("ID: %d | %s\n", p.ID, p.Name)
        fmt.Printf("  Category : %s\n", p.Category)
        fmt.Printf("  Price    : %s\n", p.FormattedPrice())
        fmt.Printf("  Stock    : %d units\n", p.Stock)

        if p.IsLowStock() {
            fmt.Printf("  ⚠️  LOW STOCK!\n")
        }

        // Simulate a sale
        err := p.Sell(3)
        if err != nil {
            fmt.Printf("  Sale failed: %v\n", err)
        } else {
            fmt.Printf("  ✓ Sold 3 units, remaining stock: %d\n", p.Stock)
        }

        // Price after a 10% discount
        discounted := calculateDiscount(p.Price, 10)
        fmt.Printf("  10%% discounted price: Rp %d\n", discounted)
        fmt.Println()
    }

    // Demonstrate type conversion
    var totalStock StockUnit
    for _, p := range products {
        totalStock += p.Stock
    }
    fmt.Printf("Total stock of all products: %d units\n", totalStock)
    fmt.Printf("As an int: %d\n", int(totalStock))
}

Summary #

  • Use int for general purposes; int64 for database IDs and timestamps; uint8/byte for binary data.
  • Integer overflow doesn’t cause a panic — the value “wraps” silently; validate ranges when needed.
  • Always use float64 unless there’s a specific reason for float32 (graphics, C interop).
  • Don’t use floats for money — store prices in integers (the smallest rupiah unit) to avoid precision errors.
  • len(string) returns the number of bytes, not characters; use []rune or range to iterate per Unicode character.
  • byte is an alias for uint8; rune is an alias for int32 used for Unicode characters.
  • string(65) produces "A" (a character), not "65" (text) — use strconv.Itoa() to convert numbers to text.
  • Pointers are needed when a function must modify the original value or for efficiency with large structs.
  • Nil pointer dereferences cause panics — always check if p != nil before dereferencing.
  • All type conversions are always explicit — Go never converts types silently.
  • Type definitions create new, incompatible types; type aliases create another name for the same type.

← Previous: Constants   Next: Operators →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact