Data Types in Go
A complete guide to Go's built-in types: integers, floats, strings, booleans, runes, bytes, and explicit type conversions.
Integer Types
Go provides signed and unsigned integers at multiple fixed sizes. Having explicit sizes matters when working with binary protocols, file formats, or external systems that specify exactly how wide a field should be. For everyday use, int is the right default — it matches the native word size of the platform and is what the standard library uses for counts and indices.
var a int8 = 127 // -128 to 127
var b int16 = 32767 // -32768 to 32767
var c int32 = 2147483647
var d int64 = 9223372036854775807
var e int = 42 // platform-width (32 or 64 bit) — use this by default
var u uint = 42 // unsigned, platform-width
var u8 uint8 = 255 // 0 to 255 (also called byte)
Which to use? Default to int for general integers. Use specific sizes when interfacing with binary protocols, files, or external systems that specify widths.
Go supports readable numeric literals with underscores as separators and alternative bases, which helps when writing values that align with hardware or protocol specifications:
// Underscores improve readability for large numbers
population := 8_000_000_000
hexColor := 0xFF5733 // hexadecimal
octal := 0o755 // octal (Unix file permissions)
binary := 0b1010_1100 // binary
Floating-Point Types
Go has two floating-point types. The difference is precision: float64 gives you about 15 significant decimal digits, while float32 gives about 7. Loss of precision in float32 can cause subtle bugs in calculations, so float64 is the right default for almost everything.
var f32 float32 = 3.14 // ~7 decimal digits of precision
var f64 float64 = 3.141592653589793 // ~15 decimal digits — use this by default
float64 is the default and almost always the right choice. Use float32 only when memory is constrained and precision can be sacrificed (e.g., graphics or large numerical arrays).
import "math"
fmt.Println(math.MaxFloat64) // 1.7976931348623157e+308
fmt.Println(math.Pi) // 3.141592653589793
fmt.Println(math.Sqrt(2)) // 1.4142135623730951
// Special float values — useful for detecting edge cases in calculations
posInf := math.Inf(1)
negInf := math.Inf(-1)
notNum := math.NaN()
fmt.Println(math.IsNaN(notNum)) // true
Boolean
The bool type holds exactly two values: true and false. Unlike C or Python, Go does not treat integers as booleans — if 1 {} is a compile error. This strictness prevents a common class of bugs where a non-zero value is accidentally treated as truthy in a context where you actually meant to check a specific condition.
var flag bool = true
active := false
// Must be a boolean expression — integers don't work as conditions
if flag {
fmt.Println("active")
}
// Logical operators produce booleans
fmt.Println(true && false) // false — AND: both must be true
fmt.Println(true || false) // true — OR: at least one must be true
fmt.Println(!true) // false — NOT: inverts the value
Strings
Strings in Go are immutable sequences of bytes encoded in UTF-8. Immutability means you can safely pass strings between goroutines and share them without copying. UTF-8 encoding means Go programs handle international text correctly by default, but it also means that a string’s byte length and its character count can differ — an important distinction covered in depth in the Strings tutorial.
s := "Hello, 世界"
fmt.Println(len(s)) // 13 — byte count, not character count
fmt.Println(s[0]) // 72 — byte value of 'H'
fmt.Println(string(s[0])) // "H"
// Raw string literals preserve backslashes and newlines literally
raw := `C:\Users\alice\go\bin`
multiline := `line one
line two
line three`
Strings support comparison operators (==, <, >) which compare lexicographically.
Rune and Byte
The byte and rune types exist to clearly distinguish between raw binary data and Unicode text. When you’re working with network protocols or file formats, you think in bytes. When you’re processing human-readable text, you think in runes (characters). Using the right type makes your intent explicit and prevents subtle bugs with multi-byte characters.
// byte is an alias for uint8 — represents raw binary or ASCII data
var b byte = 'A'
fmt.Println(b) // 65 — the ASCII value
// rune is an alias for int32 — represents one Unicode code point
var r rune = '世'
fmt.Println(r) // 19990 (Unicode code point)
fmt.Println(string(r)) // 世
// range iterates a string by rune, not by byte — handles multi-byte correctly
s := "Hello, 世界"
for i, ch := range s {
fmt.Printf("index %d: %c (U+%04X)\n", i, ch, ch)
}
// index 0: H (U+0048)
// index 7: 世 (U+4E16) ← byte index 7, not character index 7
// index 10: 界 (U+754C)
// Convert string to rune slice when you need to work with individual characters
runes := []rune(s)
fmt.Println(len(runes)) // 9 — 9 characters, not 13 bytes
Complex Numbers
Go has built-in complex number types — a relatively rare feature in mainstream languages. They are not commonly used, but they are valuable for scientific computing, signal processing, and certain mathematical algorithms where complex arithmetic would otherwise require a library.
c1 := complex(3, 4) // 3+4i — complex128 by default
c2 := 1 + 2i
fmt.Println(real(c1)) // 3
fmt.Println(imag(c1)) // 4
fmt.Println(c1 + c2) // (4+6i)
import "math/cmplx"
fmt.Println(cmplx.Abs(c1)) // 5 (magnitude — Pythagorean theorem: sqrt(3²+4²))
Type Conversions
Go never implicitly converts between types. Every conversion must be written explicitly. This feels verbose at first, but it eliminates an entire category of bugs where data is silently narrowed, widened, or reinterpreted. When you see a conversion in Go code, it is always intentional and visible.
var i int = 42
var f float64 = float64(i) // int → float64: explicit widening
var u uint = uint(f) // float64 → uint: explicit, truncates decimal part
// string(n) interprets n as a Unicode code point — this is often not what you want
n := 65
s := string(n) // "A" — treats 65 as the code point for 'A'
numStr := fmt.Sprintf("%d", n) // "65" — converts the number to its digit representation
// Use strconv for correct numeric ↔ string conversions
import "strconv"
s1 := strconv.Itoa(42) // "42" — integer to string
n1, err := strconv.Atoi("123") // 123, nil — string to integer
f1, err := strconv.ParseFloat("3.14", 64) // 3.14, nil
b1, err := strconv.ParseBool("true") // true, nil
// Format a number back to string with control over representation
s2 := strconv.FormatFloat(3.14159, 'f', 2, 64) // "3.14" — 2 decimal places
s3 := strconv.FormatInt(255, 16) // "ff" — hexadecimal
Type Aliases and Defined Types
Go lets you create new named types based on existing ones. This is a powerful tool for making code self-documenting and for preventing values of different conceptual units from being mixed accidentally. A Meters value and a Feet value are both float64 under the hood, but Go will not let you assign one to the other without an explicit conversion.
// Type alias — completely interchangeable with the original type
type Celsius = float64
type Fahrenheit = float64
// Defined type — creates a distinct type that requires explicit conversion
type Meters float64
type Feet float64
func toFeet(m Meters) Feet {
return Feet(m * 3.28084)
}
dist := Meters(100)
fmt.Println(toFeet(dist)) // 328.084
// Trying to assign Meters to Feet directly is a compile error — catches unit bugs:
// var f Feet = dist // compile error: cannot use dist (Meters) as Feet
Type Size Reference
| Type | Size | Range / Notes |
|---|---|---|
bool | 1 byte | true / false |
int8 | 1 byte | -128 to 127 |
int16 | 2 bytes | -32,768 to 32,767 |
int32 / rune | 4 bytes | -2B to 2B |
int64 | 8 bytes | -9.2e18 to 9.2e18 |
int | 4 or 8 bytes | platform-dependent |
uint8 / byte | 1 byte | 0 to 255 |
float32 | 4 bytes | ~7 significant digits |
float64 | 8 bytes | ~15 significant digits |
string | 16 bytes header | immutable byte sequence |
complex64 | 8 bytes | two float32 components |
complex128 | 16 bytes | two float64 components |
Practical Example
This example shows how types and conversions work together in a realistic calculation — parsing string inputs, converting between numeric types, and using a switch on float values to produce a string result.
package main
import (
"fmt"
"strconv"
)
func bmi(weightKg, heightM float64) float64 {
return weightKg / (heightM * heightM)
}
func classify(bmi float64) string {
switch {
case bmi < 18.5:
return "Underweight"
case bmi < 25.0:
return "Normal"
case bmi < 30.0:
return "Overweight"
default:
return "Obese"
}
}
func main() {
weightStr := "70"
heightStr := "1.75"
// strconv.ParseFloat converts a string to float64 safely
weight, _ := strconv.ParseFloat(weightStr, 64)
height, _ := strconv.ParseFloat(heightStr, 64)
b := bmi(weight, height)
fmt.Printf("BMI: %.1f — %s\n", b, classify(b))
// BMI: 22.9 — Normal
}