Go Basics #2 Variables, Types, Constants
In #1 Getting started and your first program you set up the environment — now it’s time for the language itself. Go’s basic types, ways to declare variables, and constants.
Basic types #
Go is a statically typed language. The type of every variable is fixed at compile time.
| Kind | Type |
|---|---|
| Integer | int, int8, int16, int32, int64, uint, uint8 (= byte), … |
| Floating point | float32, float64 |
| Complex | complex64, complex128 |
| Boolean | bool |
| String | string |
| Rune (Unicode code point) | rune (= int32) |
The ones you’ll use most often are int, string, bool, and float64. Use the fixed-width types (int32, int64, etc.) only when you need an exact size.
The size of int
#
int is either 32-bit or 64-bit depending on the platform. On a 64-bit OS it’s typically the same as int64. When the exact size matters, spell out int32 / int64.
Variable declarations — two ways #
1) var — explicit declaration
#
var name string = "Curtis"
var age int = 30
var ok bool = trueThe most explicit form. You write the type.
1.5) var — type inference
#
var name = "Curtis" // inferred as string
var age = 30 // inferred as intIf the type can be inferred from the initial value, you can omit the type annotation.
2) := — short declaration
#
The most common form inside functions.
name := "Curtis"
age := 30
ok := trueAlmost the same meaning as var x = ..., but it can only be used inside functions. Inside functions, := is almost always the standard convention for new variables.
:= doesn’t work outside functions
#
package main
var globalName = "Curtis" // OK
// globalName2 := "other" ✗ := doesn't work outside functions
func main() {
localName := "local" // OK
}Multiple variables at once #
var a, b, c int = 1, 2, 3
var x, y = 1, "hi" // mixed types OK
i, j := 10, 20
i, j = j, i // swapPatterns like swap feel natural with :=.
var block
#
var (
name string = "Curtis"
age int = 30
ok bool = true
)For grouping multiple var declarations together. You’ll often see this shape with package-level variables.
Zero values #
If you declare a variable without an initial value, Go initializes it with the zero value.
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string)
var p *int // nil (zero value of a pointer)A key difference from null/undefined in other languages — in Go every variable always holds a meaningful value. There’s no “declared but not initialized” state.
Type conversion — explicit #
Go almost never converts types automatically. You need an explicit conversion between different types.
i := 42
f := float64(i) // int → float64
ui := uint(f) // float64 → uint
// i + f ✗ mismatched types
i + int(f) // OKIt feels restrictive at first, but it’s a design choice that prevents accidental loss of precision.
String ↔ number #
For these you have dedicated conversion functions.
import "strconv"
s := strconv.Itoa(42) // int → string ("42")
n, err := strconv.Atoi("42") // string → int (can return an error)
f, err := strconv.ParseFloat("3.14", 64)We cover error returns in detail in #4 Functions, multiple return, error type.
Constants — const
#
If a value is fixed at compile time and never changes, use const.
const Pi = 3.14159
const MaxRetries = 3
const AppName = "MyApp"Similar to var, but — since they’re compile-time constants — they don’t change at runtime, and values like the result of a function call can’t be assigned to a const.
const Now = time.Now() // ✗ time.Now() is a runtime callTyped const vs untyped const #
const Pi = 3.14159 // untyped — type is determined by context
const PiTyped float32 = 3.14159 // typed — fixed to float32
var f float64 = Pi // OK — Pi is untyped
var f32 float32 = Pi // OK — Pi is untyped
var f32 float32 = PiTyped // OK
var f64 float64 = PiTyped // ✗ float32 → float64 needs explicit conversionUntyped consts are more flexible. Unless you have a specific reason, leaving consts untyped is the norm.
iota — auto-incrementing const
#
Inside a const group, you can produce values that auto-increment. Similar in spirit to enums in other languages.
const (
Red = iota // 0
Green // 1
Blue // 2
)iota starts at 0 within a const block and increments by 1 with each line. Automatically, without you having to repeat the same expression.
iota patterns #
// skipping values
const (
A = iota // 0
B // 1
C // 2
_ // 3 (skipped)
E // 4
)
// bit flags
const (
Read = 1 << iota // 1 (1 << 0)
Write // 2 (1 << 1)
Execute // 4 (1 << 2)
)
// units
const (
_ = iota // ignore (iota = 0)
KB = 1 << (10 * iota) // 1024
MB // 1024 * 1024
GB // 1024^3
)These patterns show up often in library code. Bit flags in particular are very common.
Named types — making types meaningful #
Instead of using a primitive type as is, you can create a type with its own name.
type UserID string
type Email string
func sendMessage(id UserID, to Email) {
// ...
}
var u UserID = "u1"
var e Email = "me@example.com"
sendMessage(u, e) // OK
// sendMessage(e, u) ✗ UserID and Email are different typesBoth are strings, but UserID and Email are not interchangeable. By baking the meaning into the type, you can prevent incorrect calls at compile time. Similar in effect to TypeScript’s branded types (TS Advanced #5).
Basic output / input — fmt
#
The fmt functions you’ll use most.
fmt.Println("automatic newline at the end of the line")
fmt.Print("no newline at the end")
fmt.Printf("Name: %s, Age: %d\n", name, age)
s := fmt.Sprintf("Result: %d", 42) // build a stringCommon format verbs:
| verb | meaning |
|---|---|
%v | default representation (any type) |
%+v | struct including field names |
%d | decimal integer |
%s | string |
%q | quoted string |
%t | bool |
%f | floating point |
%T | type name |
fmt.Printf("%d %s %t\n", 42, "hi", true) // 42 hi true
fmt.Printf("%v %T\n", 3.14, 3.14) // 3.14 float64
fmt.Printf("%q\n", "hello") // "hello"Two faces of strings #
Strings in Go are an immutable byte sequence. Indexing returns a byte.
s := "안녕"
fmt.Println(len(s)) // 6 — number of UTF-8 bytes
fmt.Println(s[0]) // 236 — first byte (a number)
// Iterate by character (rune)
for i, r := range s {
fmt.Printf("%d %c\n", i, r)
}
// 0 안
// 3 녕for range iterates rune by rune (Unicode code points). When dealing with Korean or emoji, work in runes rather than byte indices.
Common pitfalls #
1) Mixing up := and =
#
x := 10 // new variable (declare + assign)
x = 20 // reassign
y := 30 // new variable
// y := 40 ✗ y already declared (but with multiple targets, it's OK if at least one is new)In a multi-target statement, as long as at least one variable is new, := works.
x := 10
x, y := 20, 30 // x reassigned, y is new — OK2) Integer division truncates #
result := 10 / 3 // 3 (decimal truncated)
result2 := 10.0 / 3.0 // 3.333...
result3 := float64(10) / 3 // 3.333...There’s no automatic conversion as in some other languages. If you want a floating-point result, convert explicitly.
Wrapping up #
What this post covered:
- Static typing — every variable’s type is fixed at compile time
- Two declaration styles:
varand:= - Inside functions,
:=is almost the standard - Zero values — every variable gets a meaningful initial value automatically
- Almost no implicit conversion — explicit conversion required
- Use
strconvfor string ↔ number constand untyped vs typed constsiotafor auto-incrementing values, bit flags, and unit definitions- Use named types to make types meaningful
- Strings are byte sequences; iterate runes with
for range
In the next post (#3 Control flow) we cover if/for/switch and the modern patterns inside them. Go has no while; for handles all looping.