Sitemap
Devpriya Shivani

Devpriya Shivani

Go Constants

8 min readJul 3, 2024

--

Press enter or click to view image in full size
Constants in Go

In Go, constants are declared using the const keyword followed by a name and a value. It holds a unique and crucial role, distinguished by its parallel type system and remarkable precision. One interesting aspect of constants in Go is that they can be declared without specifying their type explicitly. This flexibility introduces the concept of kinds in Go's type system, which provides implicit type conversion for constants. In addition we will also get to know about iota and enumration in this blog post.

Untyped vs. Typed Constants

When a constant is declared without specifying its type, it is an untyped constant. The Go compiler assigns it a kind, such as an integer or floating-point kind. Conversely, when a constant’s type is explicitly specified, it is a typed constant. Typed constants have more restrictions compared to untyped constants.

func main() {
// Untyped Constants
const untypedInt = 1235 // kind: integer
const untypedFloat = 3.14 // kind: floating-point

// Typed Constants
const typedInt int = 1235 // type: int
const typedFloat float64 = 3.14 // type: float64

fmt.Printf("UntypedInt: \t %T %d\n", untypedInt, untypedInt)
fmt.Printf("UntypedFloat: \t %T %.2f\n", untypedFloat, untypedFloat)
fmt.Printf("TypedInt: \t %T %d\n", typedInt, typedInt)
fmt.Printf("TypedFloat: \t %T %.2f\n", typedFloat, typedFloat)
}

Output:

UntypedInt:      int 1235
UntypedFloat: float64 3.14
TypedInt: int 1235
TypedFloat: float64 3.14

The Power of Untyped Constants

Untyped constants have a unique feature that they can be implicitly converted by the Go compiler. This implicit conversion is possible due to the kind system.

func main() {
// Untyped Constants
const untypedInt = 1235 // kind: integer
const untypedFloat = 3.14 // kind: floating-point

//const Uint8 uint8 = 1000 // cannot use 1000 (untyped int constant) as uint8 value in constant declaration (overflows)
const Uint8 = 1000 // This works because it's an untyped constant[

fmt.Printf("UntypedInt: \t %T %.2f\n", untypedInt, untypedInt)
fmt.Printf("UntypedFloat: \t %T %.2f\n", untypedFloat, untypedFloat)
fmt.Printf("Uint8: \t\t %T %d\n", Uint8, Uint8)
}

Output:

UntypedInt:      int %!f(int=1235)
UntypedFloat: float64 3.14
Uint8: int 1000

In the above code, untypedInt is declared without specifying the type. Go compiler specifies it to be of kind integer. That’s why when we print it in float format, the compiler corrects its value %!f(int=1235) to be of type int.

Similarly, Uint8 can be declared as 1000 without specifying the type. If we were to define it as uint8, it would cause a compile-time error because 1000 exceeds the maximum value that a uint8 can hold (255).

Understanding Kinds and Promotion

The idea of kind in Go is fascinating because the compiler can implicitly convert constants of a kind, whereas constants of a type are restricted. When a constant is untyped, it has a kind that the compiler can promote to another kind or type as needed. This system allows for flexible and powerful type handling in Go.

Get Devpriya Shivani’s stories in your inbox

Join Medium for free to get updates from this writer.

For instance, if we declare a constant without specifying its type, it can be promoted from an integer kind to a floating-point kind, and then to a specific type:

Kind Promotion in Action

The promotion system in Go allows values of a kind to be promoted and used in various arithmetic operations. For example, an untyped integer can be promoted to a floating-point kind in arithmetic operations:

func main() {
//Variable answer will be of type float64
var answer = 3 * 0.33 //KindFloat(1)/KindFloat(3.0)

// Constant third will be of kind floating point
const third = 1 / 3.0 // KindFloat(1) / KinfFloat(3.0)

// Constant zero will be of kind integer
const zero = 1 / 3 // KindInt(1) / KindInt(3)

fmt.Printf("Answer: %T %.2v\n", answer, answer)
fmt.Printf("Third: \t %T %.2v\n", third, third)
fmt.Printf("Zero: \t %T %.2v\n", zero, zero)
}

Output:

Answer:  float64 0.99
Third: float64 0.33
Zero: int 00

Iota

The iota keyword in Go is used to create a series of related constants with a unique, incrementing value. It's beneficial when you need to define a set of constants that have a logical sequence. iota simplifies the code and makes it more readable.

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

func main() {
fmt.Printf("%d.%s\n", Sunday, "Sunday")
fmt.Printf("%d.%s\n", Monday, "Monday")
fmt.Printf("%d.%s\n", Tuesday, "Tuesday")
fmt.Printf("%d.%s\n", Wednesday, "Wednesday")
fmt.Printf("%d.%s\n", Thursday, "Thursday")
fmt.Printf("%d.%s\n", Friday, "Friday")
fmt.Printf("%d.%s\n", Saturday, "Saturday")
}

Output:

0.Sunday
1.Monday
2.Tuesday
3.Wednesday
4.Thursday
5.Friday
6.Saturday

Mathematical operations on Iota

Suppose that you want to start the value from 1 instead of 0. In that case, you can increase the iota value by 1.

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

func main() {
fmt.Printf("%d.%s\n", Sunday, "Sunday")
fmt.Printf("%d.%s\n", Monday, "Monday")
fmt.Printf("%d.%s\n", Tuesday, "Tuesday")
fmt.Printf("%d.%s\n", Wednesday, "Wednesday")
fmt.Printf("%d.%s\n", Thursday, "Thursday")
fmt.Printf("%d.%s\n", Friday, "Friday")
fmt.Printf("%d.%s\n", Saturday, "Saturday")
}

Output:

1.Sunday
2.Monday
3.Tuesday
4.Wednesday
5.Thursday
6.Friday
7.Saturday

You can also perform the logistic operation on iota.

const (
Ldate = 1 << iota // 1: Shift 1 to the left 0. 0000 0001
Ltime // 2: Shift 1 to the left 1. 0000 0010
Lmicroseconds // 3: Shift 1 to the left 2. 0000 0100
Llongfile // 4: Shift 1 to the left 3. 0000 1000
Lshortfile // 5: Shift 1 to the left 4. 0001 0000
LUTC // 6: Shift 1 to the left 5. 0010 0000
)

func main() {
fmt.Printf("%s = %d\n", "Ldate", Ldate)
fmt.Printf("%s = %d\n", "Ltime", Ltime)
fmt.Printf("%s = %d\n", "Lmicroseconds", Lmicroseconds)
fmt.Printf("%s = %d\n", "Llongfile", Llongfile)
fmt.Printf("%s = %d\n", "Lshortfile", Lshortfile)
fmt.Printf("%s = %d\n", "LUTC", LUTC)
}

Output:

Ldate = 1
Ltime = 2
Lmicroseconds = 4
Llongfile = 8
Lshortfile = 16
LUTC = 32

No Enumerators

In Go, you can’t have enumerations as you might in other languages due to the implicit conversion of kind. This can be a bit frustrating, as people often attempt to use named types to create an alias, hoping for compiler protections that don’t materialize as expected

type hold int8

func utilityFunc(h hold) {
fmt.Printf("'%v' is of type %T after function call.\n", h, h)
}

func main() {
var h hold

fmt.Printf("'%v' is of type %T initially.\n", h, h)
utilityFunc(h)
fmt.Println()
fmt.Printf("'%v' is of type %T initially.\n", 100, 100)
utilityFunc(100)
}

Output:

'0' is of type constants.hold initially.
'0' is of type constants.hold after function call.

'100' is of type int initially.
'100' is of type constants.hold after function call.

Here, you can see that the type of 100 has been implicitly converted to type constants.hold due to kind irrespective of utilityFunc definition.

Try it yourself!

1. Pass 256 instead of 100.
2. Declare a variable x of type int and try to pass it instead of h .

Instead of trying to enforce compiler protections through named types, it’s better to define constants and accept the base type directly.

const (
Red = 10
Green = 20
)

func constFunc(color int) {
fmt.Printf("'%v' is of type %T after function call.\n", color, color)
}

func main() {
fmt.Printf("'%v' is of type %T initially.\n", Red, Red)
constFunc(Red)
fmt.Println()
fmt.Printf("'%v' is of type %T initially.\n", 30, 30)
constFunc(30) // Compiler won't save you; need runtime validation
}

Output:

'10' is of type int initially.
'10' is of type int after function call.

'30' is of type int initially.
'30' is of type int after function call.

By maintaining consistency with the type system and using descriptive variable names, you can still achieve clarity and enforce constraints through runtime checks if necessary.

Complete Code

import "fmt"

type hold int8

const (
Red = 10
Green = 20
)

const (
Ldate = 1 << iota // 1: Shift 1 to the left 0. 0000 0001
Ltime // 2: Shift 1 to the left 1. 0000 0010
Lmillisecond // 3: Shift 1 to the left 2. 0000 0100
Lmicroseconds // 4: Shift 1 to the left 3. 0000 1000
Llongfile // 5: Shift 1 to the left 4. 0001 0000
Lshortfile // 6: Shift 1 to the left 5. 0010 0000
LUTC // 7: Shift 1 to the left 6. 0100 0000
)

const (
SNo1 = iota + 1 // 0
SNo2 // 1
SNo3 // 2
SNo4 // 3
SNo5 // 4
SNo6 // 5
SNo7 // 6
)

func ConstBasic() {
// Untyped Constants
const untypedInt = 1235 // kind: integer
const untypedFloat = 3.14 // kind: floating-point

// Typed Constants
const typedInt int = 1235 // type: int
const typedFloat float64 = 3.14 // type: float64

fmt.Printf("UntypedInt: \t %T %d\n", untypedInt, untypedInt)
fmt.Printf("UntypedFloat: \t %T %.2f\n", untypedFloat, untypedFloat)
fmt.Printf("TypedInt: \t %T %d\n", typedInt, typedInt)
fmt.Printf("TypedFloat: \t %T %.2f\n", typedFloat, typedFloat)
fmt.Println()
}

func UntypedConstant() {
// Untyped Constants
const untypedInt = 1235 // kind: integer
const untypedFloat = 3.14 // kind: floating-point

//const Uint8 uint8 = 1000 // cannot use 1000 (untyped int constant) as uint8 value in constant declaration (overflows)
const Uint8 = 1000 // This works because it's an untyped constant[

fmt.Printf("UntypedInt: \t %T %.2f\n", untypedInt, untypedInt)
fmt.Printf("UntypedFloat: \t %T %.2f\n", untypedFloat, untypedFloat)
fmt.Printf("Uint8: \t\t %T %d\n", Uint8, Uint8)
fmt.Println()
}

func Constant() {
//Variable answer will be of type float64
var answer = 3 * 0.33 //KindFloat(1)/KindFloat(3.0)

// Constant third will be of kinf floating point
const third = 1 / 3.0 // KindFloat(1) / KinfFloat(3.0)

// Constant zero will be of kind integer
const zero = 1 / 3 // KindInt(1) / KindInt(3)

fmt.Printf("Answer: %T %.2v\n", answer, answer)
fmt.Printf("Third: \t %T %.2v\n", third, third)
fmt.Printf("Zero: \t %T %.2v\n", zero, zero)
fmt.Println()
}

func Iota() {
fmt.Printf("%d.%s \t = %d\n", SNo1, "Ldate", Ldate)
fmt.Printf("%d.%s \t = %d\n", SNo2, "Ltime", Ltime)
fmt.Printf("%d.%s \t = %d\n", SNo3, "Lmillisecond", Lmillisecond)
fmt.Printf("%d.%s = %d\n", SNo4, "Lmicroseconds", Lmicroseconds)
fmt.Printf("%d.%s \t = %d\n", SNo5, "Llongfile", Llongfile)
fmt.Printf("%d.%s \t = %d\n", SNo6, "Lshortfile", Lshortfile)
fmt.Printf("%d.%s \t\t = %d\n", SNo7, "LUTC", LUTC)
fmt.Println()
}

func constFunc(color int) {
fmt.Printf("'%v' is of type %T after function call.\n", color, color)
}

func utilityFunc(h hold) {
fmt.Printf("'%v' is of type %T after function call.\n", h, h)
}

func NoEnumerators() {
var h hold

fmt.Printf("'%v' is of type %T initially.\n", h, h)
utilityFunc(h)
fmt.Printf("'%v' is of type %T initially.\n", 100, 100)
utilityFunc(100)
fmt.Printf("'%v' is of type %T initially.\n", Red, Red)
constFunc(Red)
fmt.Printf("'%v' is of type %T initially.\n", 30, 30)
constFunc(30) // Compiler won't save you; need runtime validation
fmt.Println()
}

func main() {
ConstBasic()
UntypedConstant()
Constant()
Iota()
NoEnumerators()
}

Output:

UntypedInt:      int 1235
UntypedFloat: float64 3.14
TypedInt: int 1235
TypedFloat: float64 3.14

UntypedInt: int %!f(int=1235)
UntypedFloat: float64 3.14
Uint8: int 1000

Answer: float64 0.99
Third: float64 0.33
Zero: int 00

1.Ldate = 1
2.Ltime = 2
3.Lmillisecond = 4
4.Lmicroseconds = 8
5.Llongfile = 16
6.Lshortfile = 32
7.LUTC = 64

'0' is of type constants.hold initially.
'0' is of type constants.hold after function call.
'100' is of type int initially.
'100' is of type constants.hold after function call.
'10' is of type int initially.
'10' is of type int after function call.
'30' is of type int initially.
'30' is of type int after function call.

Conclusion

Understanding the differences between untyped and typed constants in Go is essential for effective coding. Untyped constants offer flexibility through implicit conversions and kind promotion, making them powerful tools for generic programming. On the other hand, typed constants provide stricter type enforcement, which can prevent certain types of errors. The iota keyword further enhances the utility of constants by enabling the creation of sequences and bitwise operations. By leveraging these features and understanding their nuances, you can write more robust and maintainable Go code.

--

--