Skip to content
ioob.dev
Go back

Go Basics Part 1 — Variables, Constants, and Types

· 4 min read
Go Series (1/12)
  1. Go Basics Part 1 — Variables, Constants, and Types
  2. Go Basics Part 2 — Conditionals and Loops
  3. Go Basics Part 3 — Functions
  4. Go Basics Part 4 — Error Handling
  5. Go Basics Part 5 — Arrays, Slices, and Maps
  6. Go Basics Part 6 — Structs and Methods
  7. Go Part 7 — Interfaces
  8. Go Part 8 — Pointers
  9. Go Part 9 — Goroutines and Channels
  10. Go Part 10 — Concurrency Patterns
  11. Go Part 11 — Packages and Modules
  12. Go Part 12 — Generics and Practical Patterns
Table of contents

Table of contents

var and :=

There are two ways to declare variables in Go: using the var keyword or the short declaration operator :=.

package main

import "fmt"

func main() {
    var name string = "Go"
    age := 15  // Born in 2009, after all

    fmt.Println(name, age)
}

var can be used anywhere — inside or outside functions. On the other hand, := is shorthand syntax that can only be used inside functions. You don’t need to specify the type; it’s automatically inferred from the value on the right.

So when should you use which? When assigning a value immediately inside a function, := is much cleaner. var is for package-level variables or when you need to declare a variable without an initial value.

package main

import "fmt"

var globalMsg = "package-level variable"  // Cannot use :=

func main() {
    var count int       // Declared without initial value — initialized to 0
    name := "gopher"    // More concise inside functions

    fmt.Println(globalMsg, count, name)
}

You can also declare multiple variables at once.

var (
    host = "localhost"
    port = 8080
    debug = false
)

This block declaration is useful for grouping related variables at the package level. It’s a great pattern for organizing configuration values or constant groups.

Constants: const

Use const for values that don’t change. Since constants must be determined at compile time, you can’t assign function call results to them.

package main

import "fmt"

const pi = 3.14159
const greeting = "Hello, Go!"

const (
    statusOK    = 200
    statusNotFound = 404
)

func main() {
    fmt.Println(pi, greeting)
    fmt.Println(statusOK, statusNotFound)
}

Go constants have an interesting characteristic: the concept of untyped constants.

package main

import "fmt"

const x = 100  // Untyped — not int, not float64, just a 'number'

func main() {
    var i int = x       // Used as int
    var f float64 = x   // Used as float64 too

    fmt.Println(i, f)   // 100 100
}

Untyped constants have their type determined by the context in which they’re used. This means the same constant 100 can be assigned to both int and float64. This is a unique Go design choice that allows flexible use of constants.

iota is also worth knowing. It’s Go’s tool for creating something similar to enumerations.

package main

import "fmt"

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

func main() {
    fmt.Println(Wednesday)  // 3
}

iota starts at 0 within a const block and increments by 1 for each line. It serves a similar role to Java’s enum or consecutive C #define declarations, but it’s much more concise.

Basic Types

Go’s type system is concise. It has what you need and nothing more.

CategoryTypeDescription
Integerint, int8, int16, int32, int64int is 32-bit or 64-bit depending on the platform
Unsigned integeruint, uint8, uint16, uint32, uint64Positive numbers only
Floatfloat32, float64Use float64 as the default for decimals
Complexcomplex64, complex128For scientific computation
StringstringUTF-8, immutable
Booleanbooltrue / false
BytebyteAlias for uint8
RuneruneAlias for int32, represents a Unicode code point

In most cases, four types are enough: int, float64, string, and bool. Unless you’re working in an embedded environment where memory matters, you rarely need to specify exact bit sizes.

package main

import "fmt"

func main() {
    var age int = 25
    var height float64 = 175.5
    var name string = "gopher"
    var active bool = true

    fmt.Println(age, height, name, active)
}

The distinction between byte and rune becomes important when dealing with characters. byte represents a single ASCII character, while rune represents a single Unicode character. When working with multibyte characters like Korean, you need to use rune.

package main

import "fmt"

func main() {
    s := "Go는 재밌다"
    fmt.Println(len(s))           // 15 — byte count
    fmt.Println(len([]rune(s)))   // 7  — character count
}

When you pass a string to len(), it returns the byte count. Since each Korean character is 3 bytes, the numbers differ. If you want the actual character count, convert to []rune first and then check the length.

Type Conversion

Go does not allow implicit type conversion. This reflects Go’s philosophy. In C or Java, assigning an int to a float64 converts it automatically, but Go requires explicit conversion.

package main

import "fmt"

func main() {
    i := 42
    f := float64(i)       // int -> float64
    u := uint(f)          // float64 -> uint

    fmt.Println(i, f, u)  // 42 42 42
}

This might feel tedious at first. But considering the subtle bugs that implicit conversion can introduce, it’s actually the safer choice. It especially prevents unintended precision loss when mixing integers and floating-point numbers.

For conversion between strings and numbers, use the strconv package.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // Number -> string
    s := strconv.Itoa(42)
    fmt.Println(s)  // "42"

    // String -> number
    n, err := strconv.Atoi("123")
    if err != nil {
        fmt.Println("conversion failed:", err)
        return
    }
    fmt.Println(n)  // 123
}

Notice how Atoi returns both a value and an error. In Go, operations that can fail routinely return an error as the second return value. This will be covered in detail in Part 4 on error handling.

Zero Value

In Go, when you declare a variable without an initial value, it’s automatically assigned the zero value for its type.

package main

import "fmt"

func main() {
    var i int       // 0
    var f float64   // 0.0
    var b bool      // false
    var s string    // "" (empty string)

    fmt.Println(i, f, b, s)
}
TypeZero Value
Integer / Float0
boolfalse
string""
Pointer, slice, map, channel, function, interfacenil

This isn’t just a convenience feature — it’s a Go design principle. It eliminates the concept of “uninitialized variables” entirely. If you’ve ever struggled debugging garbage values from uninitialized variables in C, you can appreciate how valuable this decision is.

This property is actively leveraged in practice. For example, an int counter starts at 0 just by declaring it, and a bool flag defaults to false. No separate initialization code is needed, keeping the code clean.


The next part covers Go’s conditionals and loops. We’ll look at the unique syntax of adding an initializer to if, and why Go doesn’t have while.

-> Part 2: Conditionals and Loops


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Kotlin Beginner Part 5 — Collections and Lambdas
Next Post
Go Basics Part 2 — Conditionals and Loops