---
title: "Go Constants Beyond the Basics"
date: 2024-11-13
tags: ["go"]
description: "Go constants are fixed values with surprising flexibility: typed constants, iota, and compile-time tricks that matter."
---

When I first got into Go, I thought constants were simple and limited -- just fixed values, nothing fancy. But as I delved deeper, I found they're quite versatile. Yes, they're fixed values, but Go handles them in ways that are both flexible and efficient. Let's see what that means with some practical examples.

## Constants as Type-Free Values (Until They're Used)

In Go, constants are often untyped until you actually use them. They have a default kind but can be assigned to variables of different types, as long as the value fits. This makes them adaptable in a way that's unusual for a statically typed language.

Here's how that looks:

```go
const x = 10

var i int = x
var f float64 = x
var b byte = x
```

```mermaid
graph TD
    A["const x = 5\n(untyped)"] --> B["used as int"]
    A --> C["used as float64"]
    A --> D["used as byte"]
    B --> E["int"]
    C --> F["float64"]
    D --> G["byte"]
```

A bit analogous to Schrodinger's paradox, `x` can be an `int`, a `float64`, or even a `byte` until you assign it. This temporary flexibility lets `x` work smoothly with different types in your code. No need for casting, which keeps things neat.

You can even mix constants of different types in expressions and Go will figure out the best type for the result:

```go
const a = 1.5
const b = 2

const result = a * b // result is float64
```

Since `a` is a floating-point number, Go promotes the whole expression to `float64`. So you don't have to worry about losing precision -- Go handles it. But be careful: if you try to assign `result` to an `int`, you'll get an error. Go doesn't allow implicit conversions that might lose data.

## Constants in Conditional Compilation

Go doesn't have a preprocessor like some other languages, but you can use constants in `if` statements to include or exclude code at compile time.

```go
const debug = false

func main() {
    if debug {
        fmt.Println("Debugging enabled")
    }
}
```

When `debug` is `false`, the compiler knows the `if` condition will never be true and might leave out the code inside the block. This can make your final binary smaller.

## Working with Big Numbers

One powerful feature of Go's constants is that they support very large numbers. Untyped numeric constants in Go have "infinite" precision, limited only by memory and the compiler.

```go
const bigNum = 1e1000 // This is a valid constant
```

Even though `bigNum` is way bigger than any built-in numeric type like `float64` or `int`, Go lets you define it as a constant. You can do calculations with these large numbers at compile time:

```go
const (
    a = 1e20
    b = 1e30
    c = a * b // c is 1e50
)
```

## Typed Constants with iota

If you've been using Go, you've probably seen `iota` for creating enumerated constants.

```go
const (
    _ = iota
    KB = 1 << (10 * iota)
    MB
    GB
    TB
)
```

```mermaid
graph LR
    A["iota=0"] --> B["1<<0 = 1\n(Read)"]
    C["iota=1"] --> D["1<<1 = 2\n(Write)"]
    E["iota=2"] --> F["1<<2 = 4\n(Execute)"]
    B --> G["Read | Write = 3"]
    D --> G
```

## Limitations with Constants

While Go's constants are flexible, there are some things they can't do.

### 1. Constants Cannot Be Referenced by Pointers

Constants don't have a memory address at runtime. So you can't take the address of a constant or use a pointer to it.

```go
const x = 10
var p = &x // Error: cannot take the address of x
```

### 2. Function Calls in Constant Declarations

Only certain built-in functions can be used in constant expressions, like `len`, `cap`, `real`, `imag`, and `complex`.

```go
const str = "hello"
const length = len(str) // This works
const pow = math.Pow(2, 3) // Error: math.Pow cannot be used in constant expressions
```
