Go Constants
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.14The 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 1000In 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 00Iota
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.SaturdayMathematical 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.SaturdayYou 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 = 32No 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
256instead of100.
2. Declare a variablexof typeintand try to pass it instead ofh.
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.








