Cory LaNou
Wed, 10 Feb 2021

Where and When to use Iota in Go

Overview

Iota is a useful concept for creating incrementing constants in Go. However, there are several areas where iota may not be appropriate to use. This article will cover several different ways in which you can use iota, and tips on where to be cautious with it's use.

Iota is a useful concept for creating incrementing constants in Go. However, there are several areas where iota may not be appropriate to use. This article will cover several different ways in which you can use iota, and tips on where to be cautious with it's use.

Target Audience

This article is aimed at developers that are new to Go and have little to no Go experience.

Basic Iota Usage

Let's start with the most basic of usage for the iota identifier:

const (
	Red int = iota
	Orange
	Yellow
	Green
	Blue
	Indigo
	Violet
)

func main() {
	fmt.Printf("The value of Red    is %v\n", Red)
	fmt.Printf("The value of Orange is %v\n", Orange)
	fmt.Printf("The value of Yellow is %v\n", Yellow)
	fmt.Printf("The value of Green  is %v\n", Green)
	fmt.Printf("The value of Blue   is %v\n", Blue)
	fmt.Printf("The value of Indigo is %v\n", Indigo)
	fmt.Printf("The value of Violet is %v\n", Violet)
}

The above code defines three constants of type int. It then uses the iota identifier to tell the Go compiler you want the first value to start at 0, and then increment by 1 for each following constant. This results in the following output:

The value of Red    is 0
The value of Orange is 1
The value of Yellow is 2
The value of Green  is 3
The value of Blue   is 4
The value of Indigo is 5
The value of Violet is 6

Order Matters

If we take the same code as above, but change the order of the constants, we'll see the value of the constants change as well.

For instance, maybe one day a developer unfamiliar with iota comes to the code and decides that the constants would read better if they were sorted alphabetically:

const (
	Blue int = iota
	Green
	Indigo
	Orange
	Red
	Violet
	Yellow
)

func main() {
	fmt.Printf("The value of Red    is %v\n", Red)
	fmt.Printf("The value of Orange is %v\n", Orange)
	fmt.Printf("The value of Yellow is %v\n", Yellow)
	fmt.Printf("The value of Green  is %v\n", Green)
	fmt.Printf("The value of Blue   is %v\n", Blue)
	fmt.Printf("The value of Indigo is %v\n", Indigo)
	fmt.Printf("The value of Violet is %v\n", Violet)
}

As we can see when we run the program now, the value of Red is now 4 the value of Orange is 3, as well as all of the other values are now different.

$ go run main.go

The value of Red    is 4
The value of Orange is 3
The value of Yellow is 6
The value of Green  is 1
The value of Blue   is 0
The value of Indigo is 2
The value of Violet is 5

--------------------------------------------------------------------------------
Go Version: go1.22.0

Skipping Values

It may be necessary to skip a value. If so, you can use the _ (underscore) operator:

const (
	_   int = iota // Skip the first value of 0
	Foo            // Foo = 1
	Bar            // Bar = 2
	_
	_
	Bin // Bin = 5
	// Using a comment or a blank line will not increment the iota value

	Baz // Baz = 6
)

By using the underscore, we skipped 2 values between Bar and Bin. However, take note that putting a blank line will NOT increment iota, only using an underscore will increment it.

$ go run main.go

The value of Foo is 1
The value of Bar is 2
The value of Bin is 5
The value of Baz is 6

--------------------------------------------------------------------------------
Go Version: go1.22.0

Advanced Iota Usage

Because of the way iota automatically increments, you can use it to calculate more advanced scenarios. For instance, if you have worked with bitmasks, Iota can be used to quickly create the correct values by using the bit shift operator.

const (
	read   = 1 << iota // 00000001 = 1
	write              // 00000010 = 2
	remove             // 00000100 = 4

	// admin will have all of the permissions
	admin = read | write | remove
)

func main() {
	fmt.Printf("read =  %v\n", read)
	fmt.Printf("write =  %v\n", write)
	fmt.Printf("remove =  %v\n", remove)
	fmt.Printf("admin =  %v\n", admin)
}

With this, we can see we get the bitmask values printed out:

$ go run main.go

read =  1
write =  2
remove =  4
admin =  7

--------------------------------------------------------------------------------
Go Version: go1.22.0

We can take this even further and use it to calculate things like memory size. For instance, let's look at the following set of constants:

const (
	KB = 1024       // binary 00000000000000000000010000000000
	MB = 1048576    // binary 00000000000100000000000000000000
	GB = 1073741824 // binary 01000000000000000000000000000000
)

This can be rewritten with iota using both a shift operator and a multiplier

const (
	_  = 1 << (iota * 10) // ignore the first value
	KB                    // decimal:       1024 -> binary 00000000000000000000010000000000
	MB                    // decimal:    1048576 -> binary 00000000000100000000000000000000
	GB                    // decimal: 1073741824 -> binary 01000000000000000000000000000000
)

This will result in the following values:

KB =  1024
MB =  1048576
GB =  1073741824

Going Crazy with Iota

You can also pair up constants on the same line with iota. As well as you can use an underscore to skip a value within those pairs. Here is an example of going crazy with iota:

const (
	tomato, apple int = iota + 1, iota + 2
	orange, chevy
	ford, _
)

As you can see, the pairs use their respective iota definitions to calculate each following line:

tomato =  1, apple = 2
orange =  2, chevy = 3
ford   =  3

As with all programming languages, just because you can do something, doesn't mean you should.

Please do not do this in production!!!.

It is incredibly confusing code, and one of the core tenants of writing Go is to write for intent and readability. The above code fails to meet both of those expectations.

Iota in Practice

In our previous article on Leveraging the Go Type System, we saw a potential to use iota to solve the problem we presented. For review, let's look at the final solution of that article where we used a custom Type to solve for storing Genre for our books:

const (
	Adventure     Genre = 1
	Comic         Genre = 2
	Crime         Genre = 3
	Fiction       Genre = 4
	Fantasy       Genre = 5
	Historical    Genre = 6
	Horror        Genre = 7
	Magic         Genre = 8
	Mystery       Genre = 9
	Philosophical Genre = 10
	Political     Genre = 11
	Romance       Genre = 12
	Science       Genre = 13
	Superhero     Genre = 14
	Thriller      Genre = 15
	Western       Genre = 16
)

This could be re-written using iota like this:

const (
	Adventure Genre = iota
	Comic
	Crime
	Fiction
	Fantasy
	Historical
	Horror
	Magic
	Mystery
	Philosophical
	Political
	Romance
	Science
	Superhero
	Thriller
	Western
)

Now, for those of you paying close attention, you might have realized that iota always starts at 0. Which means that the value of the constant Adventure is now 0, as opposed to being 1 before. What is even more interesting, is that after making this change, the tests still passed. This is because of the way we embraced using our constants in all of our functions and tests. That's a good thing.

However, the real danger that came with this change is the fact that we likely serialized this data out at some point. Whether it was a record we saved to a database, or wrote out a json file, etc. If that had already happened, and we then modified our code later, we now will have corrupted our data, as the value of our contants for the Genre have changed.

To illustrate that point further, let's write a test. We'll use a json file that we had previously written out for a book with the following values:

func TestGenreJsonDecode(t *testing.T) {
	data := []byte(`{"ID":1,"Name":"All About Go","Genre":8}`)
	book := &Book{}
	if err := json.Unmarshal(data, book); err != nil {
		t.Fatal(err)
	}
	t.Logf("%+v", book)
	if got, exp := book.Genre, Magic; got != exp {
		t.Errorf("unexpected Genre.  got: %[1]q(%[1]d), exp %[2]q(%[2]d)", got, exp)
	}
}

Now, using the new code using iota, we'll see the test fails as the Genre is incorrect:

$ go test -v -run=TestGenreJsonDecode .

=== RUN   TestGenreJsonDecode
    books_test.go:33: &{ID:1 Name:All About Go Genre:Mystery}
    books_test.go:35: unexpected Genre.  got: "Mystery"(8), exp "Magic"(7)
--- FAIL: TestGenreJsonDecode (0.00s)
FAIL
FAIL	book	0.436s
FAIL

--------------------------------------------------------------------------------
Go Version: go1.22.0

As we can see, because we had previously serialized out the value of the Magic Genre as the value of 8, when we read the file back in, it now thinks the constant value of 8 belongs to Mystery. As a result, we have now corrupted our data.

Ok, got it, never use Iota

No, not at all. In fact, there is an easy way to fix this. We can start out our iota by adding 1 to it:

const (
	Adventure Genre = iota + 1
	Comic
	Crime
	Fiction
	Fantasy
	Historical
	Horror
	Magic
	Mystery
	Philosophical
	Political
	Romance
	Science
	Superhero
	Thriller
	Western
)

Now if we run the test, we see it is fixed.

$ go test -v  -run=TestGenreJsonDecode .
=== RUN   TestGenreJsonDecode
    books_test.go:33: &{ID:1 Name:All About Go Genre:Magic}
--- PASS: TestGenreJsonDecode (0.00s)
PASS
ok      book    0.099s

Exporting Iota constants

Because the value of a constant can be mistakenly changed using iota without realizing it, great care must be taken if you choose to use iota with exported constants. By doing this, you are stating that you will NEVER change the value of these constants. They reason for this is that you no longer know if someone that consumed your package serialized out the value of your constant. If you ever change your constant in the future (intentionally or mistakenly), you have now corrupted their data.

Iota in the Standard Library

One area that I feel really shows the power of iota is the token package in Go. There are several clever tricks they use to validate the constants.

For example, we can see they use a couple of identifiers to signify the beginning and end of a set of constant values:

literal_beg
// Identifiers and basic type literals
// (these tokens stand for classes of literals)
IDENT  // main
INT    // 12345
FLOAT  // 123.45
IMAG   // 123.45i
CHAR   // 'a'
STRING // "abc"
literal_end
see the code

They then have a function that validates any values are within that range:

func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
see the code

Note that none of these values are not exported so that there is no need to worry about future changes in the values of the constants.

Another example in the standard library is the month type:

const (
	January Month = 1 + iota
	February
	March
	April
	May
	June
	July
	August
	September
	October
	November
	December
)

Notice that these constants are in fact exported. Because of this, care has to be taken to ensure that these values never change. While it's only my opinion, I feel that the use of iota in this situation is unwarranted. It doesn't add clarity to the code, and adds unnecessary risk of a future bug.

It could be re-written like this to ensure that the values don't change and adds clarity to the code.

const (
	January   Month = 1
	February  Month = 2
	March     Month = 3
	April     Month = 4
	May       Month = 5
	June      Month = 6
	July      Month = 7
	August    Month = 8
	September Month = 9
	October   Month = 10
	November  Month = 11
	December  Month = 12
)

In this case, using direct constants would be a more readable and robust solution.

Summary

As you can see, the iota identifier in Go can be used in many different scenarios. And while that makes using iota a very powerful concept, care must also be taken to ensure that future changes won't result in the corruption of data. If you take one and only one thing away from this article, it's that if you export any constants that use iota, you must have robust testing around those constants to ensure no future changes can be made to those constants.

To really summarize, do not use iota if:

  • The constants defined by iota are exported
  • The constants defined by iota will ever be serialized and deserialized (for example, saved to a file and read back in).

Want More?

If you've enjoyed reading this article, you may find these related articles interesting as well:

More Articles

Quick Tips: Pointer Optimizations in Go

Overview

This article explores important performance considerations when working with pointers in Go. We'll cover key topics like returning pointers to local variables, choosing between pointer and value receivers for methods, and how to properly measure and optimize pointer-related performance using Go's built-in tools. Whether you're new to Go or an experienced developer, these tips will help you write more efficient and maintainable code.

Learn more

Hype Quick Start Guide

Overview

This article covers the basics of quickly writing a technical article using Hype.

Learn more

Writing Technical Articles using Hype

Overview

Creating technical articles can be painful when they include code samples and output from running programs. Hype makes this easy to not only create those articles, but ensure that all included content for the code, etc stays up to date. In this article, we will show how to set up Hype locally, create hooks for live reloading and compiling of your documents, as well as show how to dynamically include code and output directly to your documents.

Learn more