Go Data Types: A Comprehensive Guide for Developers
One of the fundamental aspects of any programming language is its type system. In Go (Golang), data types are designed to be simple, explicit, and efficient. Understanding Go’s type system is essential for writing reliable, well-optimized applications.
This guide covers every primary data type in Go — from primitives to composite types like slices, maps, and interfaces — with examples and best practices.
Primitive Types
Boolean (bool)
Represents true or false. Used in control flow — if statements, loops, conditions.
var isGoAwesome bool = true
Go does not allow implicit conversions between numeric and boolean types.
if 1 { }is a compile error — you must writeif count > 0 { }.
String (string)
An immutable sequence of bytes encoded in UTF-8. Defined with double quotes.
var greeting string = "Hello, Go!"
greeting := "Hello, " + "World!" // concatenation with +
For multi-line strings, use backtick raw string literals:
query := `SELECT *
FROM users
WHERE active = true`
Integer Types
Go provides multiple integer types with specific bit widths.
Signed integers:
| Type | Size | Range |
|---|---|---|
int | 32 or 64-bit (platform) | Platform-dependent |
int8 | 8-bit | -128 to 127 |
int16 | 16-bit | -32,768 to 32,767 |
int32 | 32-bit | -2,147,483,648 to 2,147,483,647 |
int64 | 64-bit | -9.2 × 10¹⁸ to 9.2 × 10¹⁸ |
Unsigned integers:
| Type | Size | Range |
|---|---|---|
uint | Platform | 0 to max |
uint8 | 8-bit | 0 to 255 |
uint16 | 16-bit | 0 to 65,535 |
uint32 | 32-bit | 0 to 4,294,967,295 |
uint64 | 64-bit | 0 to 18.4 × 10¹⁸ |
Use int for general-purpose integers. Only reach for int8/int16 when memory optimization is critical (e.g., large arrays of small numbers).
Byte and Rune
byteis an alias foruint8. Used for raw binary data or ASCII characters.runeis an alias forint32. Represents a Unicode code point — use it when working with text that may contain non-ASCII characters.
var letter byte = 'A' // ASCII
var emoji rune = '🚀' // Unicode code point U+1F680
Floating-Point Types
float32: 32-bit single precisionfloat64: 64-bit double precision (default for float literals)
var pi float64 = 3.141592653589793
var e float32 = 2.71828
When in doubt, use float64 — it’s the default and provides higher precision.
Complex Number Types
Go has built-in support for complex numbers:
var c complex128 = complex(3.0, 4.0) // 3 + 4i
fmt.Println(real(c)) // 3
fmt.Println(imag(c)) // 4
Rarely used in web development, but valuable for scientific computing.
Composite Types
Arrays
An array is a fixed-length sequence of elements of the same type. The length is part of the type — [3]int and [5]int are different types.
var scores [3]int = [3]int{95, 87, 72}
// or using short declaration:
names := [3]string{"Alice", "Bob", "Charlie"}
fmt.Println(names[0]) // Alice
fmt.Println(len(names)) // 3
Use [...] to let the compiler infer the length:
primes := [...]int{2, 3, 5, 7, 11}
Arrays in Go are values, not references. Assigning an array to another variable copies all elements. For large arrays, this is expensive — use slices instead.
Slices
Slices are the most important composite type in Go. A slice is a dynamically-sized, flexible view into an underlying array.
// Create a slice literal
fruits := []string{"apple", "banana", "cherry"}
// Append elements
fruits = append(fruits, "date")
// Slice a slice (half-open range)
fmt.Println(fruits[1:3]) // ["banana", "cherry"]
// Length and capacity
fmt.Println(len(fruits)) // 4
fmt.Println(cap(fruits)) // varies (Go manages this internally)
make() for Pre-allocated Slices
When you know the size upfront, use make to avoid repeated re-allocations:
// make([]Type, length, capacity)
nums := make([]int, 0, 100) // empty slice, capacity 100
Iterating Over a Slice
for i, v := range fruits {
fmt.Printf("Index %d: %s\n", i, v)
}
Slice vs Array: When to Use Which
| Array | Slice | |
|---|---|---|
| Length | Fixed at compile time | Dynamic |
| Passed to functions | By value (copy) | By reference (to underlying array) |
| Use case | Fixed-size buffers, matrices | General-purpose sequences |
Default to slices. Use arrays only when you need a fixed-size data structure or when embedding in a struct.
Maps
A map is an unordered collection of key-value pairs. Keys must be comparable types (string, int, bool — but not slices or maps).
// Map literal
scores := map[string]int{
"Alice": 95,
"Bob": 87,
}
// Reading a value
fmt.Println(scores["Alice"]) // 95
// Adding / updating
scores["Charlie"] = 72
// Deleting
delete(scores, "Bob")
// Check if key exists (comma-ok idiom)
value, ok := scores["Bob"]
if !ok {
fmt.Println("Bob not found")
}
Creating Maps with make
cache := make(map[string][]string) // map of string → string slice
Never read from a nil map without initializing it. Reading from a nil map returns the zero value. Writing to a nil map panics.
var m map[string]int // nil map
m["key"] = 1 // PANIC: assignment to entry in nil map
m = make(map[string]int) // initialize first
m["key"] = 1 // OK
Iterating Over a Map
for key, value := range scores {
fmt.Printf("%s: %d\n", key, value)
}
Note: map iteration order is random in Go by design. Never rely on a specific iteration order.
Pointers
A pointer holds the memory address of a value. Go uses pointers explicitly, unlike JavaScript or Python where object references are implicit.
x := 42
p := &x // p is a pointer to x (type: *int)
fmt.Println(p) // prints memory address: 0xc0000b4008
fmt.Println(*p) // dereferences pointer: 42
*p = 100 // modifies x through the pointer
fmt.Println(x) // 100
When to Use Pointers
Modify a value inside a function:
func double(n *int) {
*n *= 2
}
x := 5
double(&x)
fmt.Println(x) // 10
Avoid copying large structs:
// Passing a large struct by value copies all fields — expensive
func processLarge(s LargeStruct) { ... }
// Passing a pointer avoids the copy
func processLarge(s *LargeStruct) { ... }
Represent optional values: A nil pointer represents the absence of a value — Go’s equivalent of nullable.
func findUser(id int) *User {
// return nil if not found
return nil
}
Go does not have pointer arithmetic like C. You can’t increment a pointer to move through memory — this is intentional for safety.
Interfaces
An interface defines a set of method signatures. Any type that implements all the methods satisfies the interface — implicitly, with no implements keyword needed.
type Shape interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
Both Rectangle and Circle satisfy Shape without any explicit declaration:
func printShape(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
printShape(Rectangle{Width: 5, Height: 3})
printShape(Circle{Radius: 4})
The Empty Interface
interface{} (or any in modern Go) accepts any type:
func printAnything(v any) {
fmt.Println(v)
}
printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})
Use it sparingly — it loses type safety.
Type Conversion
Go is strongly typed — you must convert explicitly between types:
var i int = 42
var f float64 = float64(i) // int → float64
var u uint = uint(f) // float64 → uint
var s string = strconv.Itoa(i) // int → string (not string(i)!)
var n int
n, _ = strconv.Atoi("123") // string → int
string(65)gives you"A"(the ASCII character), not"65". Usestrconv.Itoato convert integers to their string representation.
Type Inference
The := short declaration infers the type from the assigned value:
name := "Golang" // string
age := 30 // int
ratio := 3.14 // float64
isDeveloper := true // bool
Types remain strict at compile time — inference just removes the verbosity of explicit declarations.
Key Takeaways
- Use
intfor general integers,float64for decimals,stringfor text. - Use
runefor Unicode characters,bytefor raw binary data. - Arrays are fixed-size value types. Slices are dynamic and the default choice for sequences.
- Maps hold key-value pairs. Always initialize with
makeor a literal before writing. - Pointers enable mutation inside functions and avoid expensive struct copies. Go has no pointer arithmetic.
- Interfaces are satisfied implicitly — any type with the right methods qualifies. Prefer narrow interfaces.
- All type conversions must be explicit — Go never performs implicit numeric coercion.
Once you’re comfortable with types, the natural next step is Go structs — how to group these types into custom data models — and creating your first Go module to package and share your code.
Related Articles
Understanding Structs in Go: The Foundation of Data Modeling
Learn how Go structs work — defining custom types, creating instances, passing by value vs pointer, adding methods, constructor functions, struct tags for JSON, anonymous structs, and struct embedding.
GoCreating Your First Go Module: A Step-by-Step Tutorial
Learn how to create, link, and publish Go modules — build a reusable math utilities module, use go mod init, the replace directive, go workspaces, semantic versioning, and publish to pkg.go.dev.
GoDemystifying Go's fmt.Sprintf: A Practical Guide
Master Go's fmt.Sprintf() with format verbs (%v, %s, %d, %q, %b, %x), width and padding, zero-padding, argument indexing, fmt.Fprintf for HTTP responses, fmt.Sscanf for parsing, and common pitfall fixes.
Written by
Aditya RawasFull-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.