Memory Layouts

This articles will introduce type alignment guarantees in Go. It is essential to know the guarantees to properly use the 64-bit functions in sync/atomic standard package.

Go is a C family language, so you will find many concepts talked in this article are shared with C.

Type Alignment Guarantees In Go

Type alignment (or value address alignment) guarantees are the requirements for Go compilers made by Go specification. If the alignment guarantee of a type T is N, then the address of every value of type T must be a multiple of N at run time. We can also say the addresses of values of type T are promised to be N-byte aligned.

In fact, each type has two alignment guarantees, one is for when it is used fields of other (struct) types, the other is for other cases (when it is used for a variable declaration, array element type, etc). We call the former one the field alignment guarantee of that type, and call the latter one the general alignment guarantee of that type.

At compile time, for a type T, we can call unsafe.Alignof(t) to get its general alignment guarantee, where t is a non-field value of type T, and call unsafe.Alignof(x.t) to get its field alignment guarantee, where x.t is a field value of type T.

At run time, for a value t of type T, we can call reflect.ValueOf(t).Type().Align() to get the general alignment guarantee of type T, and call reflect.ValueOf(t).Type().FieldAlign() to get the field alignment guarantee of type T.

For the current standard Go compiler, the field alignment guarantee and the general alignment guarantee of a type are always equal. For gccgo compiler, the statement is false.

Following of the article will use alignment guarantee to refer to either guarantee.

Go specification only mentions a little on type alignment guarantees:
The following minimal alignment properties are guaranteed:
  1. For a variable x of any type: unsafe.Alignof(x) is at least 1.
  2. For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  3. For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type..

The guarantees don't specify the exact alignment guarantees for any types. It just specifies some minimal requirements. For the same compiler, the exact type alignment guarantees may be different between different architectures and between different compiler versions.

For the current version (v1.11) of the standard Go compiler, the alignment guarantees are listed here:
type                               alignment guarantee
------                             ------
bool, byte, uint8, int8            1
uint16, int16                      2
uint32, int32, float32, complex64  4
arrays and structs                 depend on element types and field types
other types                        size of a native word

Here, a native word (or machine word) is 4-byte sized on 32-bit architectures and 8-byte sized on 64-bit architectures.

This means, for the current version of the standard Go compiler, the alignment guarantees of other types may be either 4 or 8, depends on the build target.

Generally, we don't need to care about the value address alignments in Go programming, except that we want to write portable programs and use the 64-bit functions from sync/atomic package.

64-bit Word Atomic Assessment Guarantees

(In this article, 64-bit words mean values of types whose underlying types are int64 or uint64.)

At the end of the sync/atomic documentation, it mentions:
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX.

On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.

On both ARM and x86-32, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

(Here, 64-bit in the last sentence is appended by context. Although the above quoted texts shown in the bug zone of that documentation, what they describe are not bugs.)

So, on very old CPU architectures, 64-bit atomic functions are not supported. Programs will panic when 64-bit atomic functions are called on such platforms. If a program is expected to support these architectures, sync.Mutex should be used instead to synchronize 64-bit words. Methods of sync.Mutex are less efficient than atomic functions.

On 64-bit CPU architectures, 64-bit word values are always 8-byte aligned, so we can call the 64-bit atomic functions safely.

What we do need to pay attention to is 64-bit word atomic assessments on some not-very-old 32-bit architectures. 64-bit atomic functions are supported on these architectures, but the addresses of 64-bit word values are not guaranteed to be 8-byte aligned on these architectures. Calls of 64-bit atomic functions for 64-bit words which addresses are not 8-byte aligned may panic at run time.

However, on the not-very-old 32-bit architectures, the official documentation guarantees that the first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

What does the word allocated mean? You can think an allocated value as a declared variable or a value returned by the new and make functions. If a slice value derives from an alloacted array and the first element of the slice is the first element of the array, then the slice value can also be viewed as an alloacted value.

The guarantees made by the official documentation is very conservative. In fact, if a value is located at the start address of a memory block allocated on heap, then the first word in the value can be relied upon to be 64-bit aligned.

And be relied upon to be 8-byte aligned doesn't mean the address of the first 64-bit word in a global variable or in an allocated struct or array is always 8-byte aligned on 32-bit architectures. It just means if the first 64-bit word in such a value is ever accessed atomically, then compilers must guarantee the memory address of that value is 8-byte aligned at run time. If it is never accessed atomically, its address is not guaranteed to be 8-byte aligned.

There are more other 64-bit words which can also be accessed atomically. It is just some subtle and verbose to make a simple clear definition in wording, so official documentations just makes conservative guarantees.

By common sense, a qualified compiler should promise that all elements in an array/slice can also be accessed atomically if the element type of the array/slice is a 64-bit word type and there is one element in the array/slice could be accessed atomically, though official documentations doesn't make this guarantee.

Here is an example which lists some 64-bit words which are safe or unsafe to be accessed atomically:
type (
	T1 struct {
		v uint64
	}

	T2 struct {
		_ int16
		x T1
		y *T1
	}

	T3 struct {
		_ int16
		x [6]int64
		y *[6]int64
	}
)

// below, the "safe"s in comments mean "safe to be accessed atomically
// on both 64-bit and 32-bit architectures".

var a int64    // a is safe
var b T1       // b.v is safe
var c [6]int64 // c[0] is safe

var d T2 // d.x.v is unsafe
var e T3 // e.x[0] is unsafe

func f() {
	var f int64           // f is safe
	var g = []int64{5: 0} // g[0] is safe

	var h = e.x[:] // h[0] is unsafe

	// here, d.y.v and e.y[0] are both safe, for they are allocated.
	d.y = new(T1)
	e.y = &[6]int64{}

	_, _, _ = f, g, h
}

// In fact, all elements in c, g and e.y.v are safe to be accessed
// atomically, though the official documentations don't make the
// guarantees.

Package maintainers should beware of exposing types with 64-bit fields accessed atomically. For example, it is unsafe to embed the Int and Float types from the expvar standard package in user code which would run on 32-bit architectures.

If you can't make sure whether or not a 64-bit word can be accessed atomically, you can use a value of type [15]byte to determine the address for the 64-bit word at run time:
package mylib

import (
	"unsafe"
	"sync/atomic"
)

type Counter struct {
	x [15]byte // instead of "x uint64"
}

func (c *Counter) xAddr() *uint64 {
	// the return must be 8-byte aligned.
	return (*uint64)(unsafe.Pointer(
		uintptr(unsafe.Pointer(&c.x)) + 8 -
		uintptr(unsafe.Pointer(&c.x))%8))
}

func (c *Counter) Add(delta uint64) {
	p := c.xAddr()
	atomic.AddUint64(p, delta)
}

func (c *Counter) Value() uint64 {
	return atomic.LoadUint64(c.xAddr())
}

By using this solution, the Counter type can be embedded in other user types safely, even on 32-bit architectures. The drawback of this solution is there are seven bytes being wasted for every value of Counter type. The sync standard package uses a [3]uint32 value to do this trick instead. This trick work for both the standard Go compiler and gccgo but might not work for another third-party Go compiler.

Future Possible Change

Russ Cox, one core Go team member, has proposed that the addresses of 64-bit words should always be 8-byte aligned, either on 64-bit or 32-bit architectures, to make Go programming simpler. Currently (Go 1.11), this proposal hasn't been adopted yet.

Type Value Sizes And Structure Field Padding

Go specification only makes following type size guarantees:
type                               size in bytes
------                             ------
byte, uint8, int8                   1
uint16, int16                       2
uint32, int32, float32              4
uint64, int64, float64, complex64   8
complex128                         16

Go specification doesn't make value size guarantees for other types. For the current version (v1.11) of the standard Go compiler, bool values are one-byte sized and int/uint/uintptr/pointer values are one-native-word sized. The full list of value sizes of different types settled by the standard Go compiler are listed in this article.

The standard Go compiler will ensure the size of values of a type is a multiple of the alignment guarantee of the type.

To satisfy type alignment guarantees mentioned in previous sections, Go compiler and runtime may pad some bytes between fields of struct values. This makes the value size of a struct type may be not a simple sum of the sizes of all fields of the type.

Here is an example showing how bytes are padded between struct fields:
// The alignments of type T1 and T2 are the same as
// the largest alignment of their field types (int64),
// 8 on AMD64 OS and 4 on i386 OS.

type T1 struct {
	a int8
	// To make b 8-aligned on AMD64 OS and 4-aligned on i386 OS,
	// 7 bytes padded on AMD64 OS and pad 3 bytes padded on i386 OS here.
	b int64
	c int16
	// To make the size of T1 values is a multiple of the alignment of T1,
	// 6 bytes padded on AMD64 OS and pad 2 bytes padded on i386 OS here.
}
// the sizes of T1 values are 24 on AMD64 OS and 16 on i386 OS.

type T2 struct {
	a int8
	// To make c 2-aligned,
	// 1 byte padded on both AMD64 and i386 OS here.
	c int16
	// To make b 8-aligned on AMD64 OS and 4-aligned on i386 OS,
	// 4 bytes padded on AMD64 OS here. No padding on i386 OS.
	b int64
}
// the sizes of T2 values are 16 on AMD64 OS and 12 on i386 OS.

Although T1 and T2 have the same field set, their value sizes are different.

One interesting fact for the standard Go compiler is that sometimes zero sized fields may affect structure padding. Please read this question in the unoffcial Go FAQ for details.


The Go 101 project is hosted on both Github and Gitlab. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

Support Go 101 by playing Tapir's games. Cryptocurrency donations are also welcome:
Bitcoin: 1xucQbv5jujFPPwhyg395ri5yV71hx9g9
Ethereum: 0x5dc4aa2c2bbfaadae373dadcfca11b3358912212