[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$pC4ZxlFnru":3,"blog-post-high-performance-golang-struct-optimizations-paddings-and-alignments":4},"email-vzae8bsf",{"content":5,"created_at":6,"description":7,"id":8,"keywords":9,"reading_time":13,"slug":14,"title":15,"updated_at":16,"ok":17},"# High-performance Golang struct optimizations: Paddings and Alignments.\n\nWhen we're creating structs in Golang, we typically think about the fields we need and their types, the readability and maintainability. Sometimes we sort fields by their name, sometimes by their logic, and sometimes we just put them in the order we like. But what if I tell you that the order of fields in Golang struct dramatically affects the memory usage and performance of your application?\n\nIn certain scenarios, you can **reduce RAM usage** by your application by **20-50%** and increase allocation and access performance **for free**. To achieve this, we need to understand how the Golang compiler works with paddings and alignments in structs.\n\n## Struct Layout.\n\nThe compiler in Golang uses a specified logic for struct layout to make it more efficient in terms of memory access and optimal alignment for target hardware architecture. The compiler will align struct fields to the size of the largest field type in the struct, and it will also **add padding bytes between fields** to ensure that each field is aligned correctly.\n\nSo if your struct has **2 fields**, one of type `int32` and another of type `int64`, the compiler will align the `int64` field to **8 bytes** boundaries, and it will add **4 bytes of padding** after the `int32` field to ensure that the `int64` field is aligned correctly. This 8 byte boundary size applies to 64-bit architecture, which is the most common architecture used today. 64 bits equals 8 bytes, we can say that 64-bit architecture [word](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWord_(computer_architecture)) size is 8 bytes and 32-bit architecture word size is 4 bytes.\n\nHowever, if the underlying type size is greater than the word size, the compiler will still align it to our architecture word size (8 bytes in our case). For example, if you have a string field of size **30 bytes**, the compiler will align it to 8 bytes boundaries making it look like 4 chunks: 3 chunks of 8 bytes and 1 chunk of 6 bytes plus **2 padding bytes** to make it **32 bytes in total**.\n\nThose padding bytes are not used by the application, but they are still allocated in memory, which can lead to increased memory usage and decreased performance. What can you possibly put in 2 bytes? It can be 2 fields of `int8` or `byte` type, or even `bool`; one field of `int16` and `uint16` etc.\n\nSo, the order of fields in your structure will affect the number of **padding bytes** added by the compiler **between** them and the overall memory usage of your application. The more padding bytes you have, the more memory your application will use, and the slower it will be.\n\n## How paddings are born.\n\nLet's take a look at a simple structure example first:\n\n```go\n  \u002F\u002F NestedLayout represents a nested structure simple example.\n  \u002F\u002F Size: 24 bytes\n  type NestedLayout struct {\n    ID    int64 \u002F\u002F 8 bytes\n    Phone int64 \u002F\u002F 8 bytes\n    Age   int32 \u002F\u002F 4 bytes\n    \u002F\u002F 4 bytes padding to align the structure to 8 bytes\n  }\n\n  \u002F\u002F UnoptimizedLayout - not optimized for size and performance.\n  \u002F\u002F Size: 96 bytes\n  type UnoptimizedLayout struct {\n    AreaID         int32        \u002F\u002F 4 bytes\n    IsActive       bool         \u002F\u002F 1 byte\n    BalanceInCents int64        \u002F\u002F 8 bytes\n    IsSpecial      bool         \u002F\u002F 1 byte\n    IdempotencyKey int64        \u002F\u002F 8 bytes\n    ID             uint32       \u002F\u002F 4 bytes\n    User           NestedLayout \u002F\u002F 24 bytes\n    IsMigrated     bool         \u002F\u002F 1 byte\n    CreatedAt      int32        \u002F\u002F 4 bytes\n    Status         uint16       \u002F\u002F 2 bytes\n    Key            float64      \u002F\u002F 8 bytes\n    TenantID       int8         \u002F\u002F 1 byte\n    UpdatedAt      int32        \u002F\u002F 4 bytes\n  }\n```\n\nThe structure `UnoptimizedLayout` may look normal to the untrained eye, but it has a lot of padding bytes between fields. The total size of the structure is **96 bytes after compiler** optimizations. But if you manually calculate the sum of all the fields you will get **70 bytes**. The size of your structure increased by **26 padding bytes**. You can check the size of this structure for yourself using `Sizeof` function from `unsafe` package:\n\n```go\n  func main() {\n    fmt.Printf(\"UnoptimizedLayout size: %d bytes\\n\", unsafe.Sizeof(UnoptimizedLayout{}))\n  }\n```\n\n> UnoptimizedLayout size: 96 bytes\n\n### aligo: visualizing paddings of your structure.\n\nTo visualize paddings and alignments of your structure, I recommend using [aligo tool](https:\u002F\u002Fgithub.com\u002Fessentialkaos\u002Faligo). It is a simple command line tool that can help you to visualize the layout of your structures in Golang and help you to optimize it. You can install it using `go install` command:\n\n```bash\ngo install github.com\u002Fessentialkaos\u002Faligo\u002Fv2@latest\n```\n\nNext, you can run it against your structure:\n\n```bash\naligo -s UnoptimizedLayout view main.go\n```\n\nThis will output a visual representation of your structure with paddings and alignments:\n\n![aligo: UnoptimizedLayout](https:\u002F\u002Fgozman.space\u002Fimg\u002Fposts\u002Fgolang-paddings-june-2025\u002Faligo-unoptimized.png)\n\nPaddings marked with red color can be optimized by changing the order of fields in your structure. Let's take a look at another `aligo` command that will help us to optimize our structure:\n\n```bash\naligo -s UnoptimizedLayout check main.go\n```\n\nThis will output a sorted struct with optimized paddings and alignments:\n\n![aligo: UnoptimizedLayout optimized](https:\u002F\u002Fgozman.space\u002Fimg\u002Fposts\u002Fgolang-paddings-june-2025\u002Faligo-check-unoptimized.png)\n\nYou can see that we just changed the order of fields and reduced the size of our structure from 96 bytes to 72 bytes - **25% memory usage reduction**.\n\n### aligo: comparing structures.\n\nLet's rename our optimized structure to `OptimizedLayout` and run `aligo view` to visualize the difference:\n\n![aligo: OptimizedLayout](https:\u002F\u002Fgozman.space\u002Fimg\u002Fposts\u002Fgolang-paddings-june-2025\u002Fpaddings-difference.jpg)\n\nYou can see that the number of padding bytes is reduced from 26 to 2 bytes. The total size of the structure is reduced from 96 bytes to 72 bytes, which is a **25% memory usage reduction**. We still have 2 padding bytes left, we can add 2 more boolean fields in our structure for free, and we will still have the same size of 72 bytes.\n\nIf you have a million of those records sitting in your memory, your unoptimized structure will use **96 MB** of memory, while the optimized one will use only **72 MB**. This is a **24 MB** difference, which is a pretty significant number for a million records.\n\n## Benchmarks.\n\nNow that we know how to optimize our structures, let's take a look at the performance difference between them.\n\n```go\n  func Benchmark(b *testing.B) {\n    k := 1000000 \u002F\u002F Number of records to allocate and access\n    unoptimized := make([]UnoptimizedLayout, k)\n    optimized := make([]OptimizedLayout, k)\n\n    b.Run(\"MemoryAllocation-UnoptimizedLayout\", func(b *testing.B) {\n      for b.Loop() {\n        _ = make([]UnoptimizedLayout, k)\n      }\n    })\n\n    b.Run(\"MemoryAllocation-OptimizedLayout\", func(b *testing.B) {\n      for b.Loop() {\n        _ = make([]OptimizedLayout, k)\n      }\n    })\n\n    b.Run(\"FieldAccess-UnoptimizedLayout\", func(b *testing.B) {\n      for i := 0; b.Loop(); i++ {\n        for j := range unoptimized {\n          unoptimized[j].BalanceInCents = int64(i + j)\n        }\n      }\n    })\n\n    b.Run(\"FieldAccess-OptimizedLayout\", func(b *testing.B) {\n      for i := 0; b.Loop(); i++ {\n        for j := range optimized {\n          optimized[j].BalanceInCents = int64(i + j)\n        }\n      }\n    })\n  }\n```\n\nNow run the benchmarks using `go test -bench=. -benchmem` command and you will get similar output:\n\n```bash\nBenchmark\u002FMemoryAllocation-UnoptimizedLayout-14  2470  447593  ns\u002Fop  96002053 B\u002Fop 1 allocs\u002Fop\nBenchmark\u002FMemoryAllocation-OptimizedLayout-14    3802  314902  ns\u002Fop  72007681 B\u002Fop 1 allocs\u002Fop\nBenchmark\u002FFieldAccess-UnoptimizedLayout-14        948  1118201 ns\u002Fop  0        B\u002Fop 0 allocs\u002Fop\nBenchmark\u002FFieldAccess-OptimizedLayout-14         1202  927754  ns\u002Fop  0        B\u002Fop 0 allocs\u002Fop\n```\n\nOf course, the numbers may vary depending on your hardware and Go version, but you can see that the optimized structure has better memory allocation performance and field access performance. The memory allocation time for unoptimized structure is **447593 ns\u002Fop** while for optimized structure it is **314902 ns\u002Fop**. The field access time for unoptimized structure is **1118201 ns\u002Fop** while for optimized structure it is **927754 ns\u002Fop**. This is a **30%** and **17%** performance improvement respectively for 1 million records. Absolutely for free!\n\n> How to read benchmarks and what else you can bench I described in my article [Optimization Odyssey: pprof-ing & Benchmarking Golang App](https:\u002F\u002Fgozman.space\u002Fblog\u002Foptimization-odyssey-profiling-and-benchmarking-golang-app-with-pprof).\n\n## The elephant in the room.\n\nLet's take a look at the elephant in the room. The **elephants**, to be precise. Types that I never mentioned in this article on purpose.\n\n**Pointer** - pointer to the structure in Golang only takes **8 bytes** of memory on 64-bit architecture. But it obscures the real size of the structure it points to and adds additional job for the garbage collector as well.\n\n**Interface** - the interface type in Golang is a reference type, which means that it does not have a fixed size and can be used to store any type that implements the interface. We can't say for sure ho much memory it will use because it depends on the implementation. But we can say that the interface pointer in Golang is **16 bytes**. Why 16 bytes and not 8? Because Golang uses **pointer tagging** to store additional information about the pointer, like the type of the value it points to. This is done to make the garbage collector more efficient and to allow for more flexible memory management. So, if you have a structure with an interface field, it will use **16 bytes** for the _interface_ and additional memory for the implementation it points to.\n\n**String** - the string type in Golang is also a reference type, which means that it does not have a fixed size and can be used to store any string value. The string type in Golang is implemented as a struct with two fields: a pointer to the string data and the length of the string. The pointer is **8 bytes** (`uintptr` type) and the length is **8 bytes** (`int` type), so the **minimum size** of the string type is **16 bytes**. This means that if you have a structure with a _string_ field, it will use **16 bytes** for the pointer and the length of the string, plus additional memory for the string data itself.\n\n**Slice** - the slice type in Golang is also a reference type, which means that it does not have a fixed size and can be used to store any slice value. Similar to the string type, the slice type in Golang is implemented as a struct with three fields: a pointer to the slice data, the length of the slice, and the capacity of the slice.\n\nThe same goes for the _map_ type in Golang and others. I avoided them here on purpose because it is hard to say how much memory they will use in this educational article. It will depend on the implementation and the data you put in them, and it opens doors to more optimizations and considerations.\n\n## Some tips and considerations.\n\nNow, when you know how to optimize your structures in Golang, you probably wonder why the compiler does not do it for you **automatically**? Don't we have a compiler flag for that? The answer is quite simple: the compiler does not know **the context** of your application and the logic behind your structures. You can use your structures for serialization and deserialization, for database operations, for network communication, etc. The compiler cannot know if the **order of the fields** in your structure is **important** for your application logic.\n\nAt this time, I need to stop you to  **think**. Should you really optimize your structures? In most cases, the answer is **no**. The performance gain will be more visible only in certain cases, like working with large datasets, parallel batch processing with workflows, or high-performance applications. If you are working on a simple HTTP router - most likely you don't notice any difference in performance.\n\nBut let's say you want to make a habit of optimizing your structures for some reason. The rule of thumb here is to group the fields by their size, moving the largest fields to the top of the structure and the smallest fields to the bottom. This might be enough for most cases. You can also use the help of the `govet` linter in [golangci-lint](https:\u002F\u002Fgolangci-lint.run\u002Fusage\u002Flinters\u002F#govet) tool that you probably already use in your project. Just enable `fieldalignment` option for `govet` in your `.golangci.yml` configuration file. But it might be overkill.\n\nYou can also call this linter manually, since it is using a tool from the Golang standard library under the hood:\n\n```bash \ngo install golang.org\u002Fx\u002Ftools\u002Fgo\u002Fanalysis\u002Fpasses\u002Ffieldalignment\u002Fcmd\u002Ffieldalignment@latest\nfieldalignment -fix main.go\n```\n\nI hope this article inspired you to take a look at your code and find some places where you can benefit from padding optimizations. If you are working with large datasets - **every byte counts**.","2025-06-14T19:47:01.27305Z","How can you reduce Go structs memory usage by 20-50% and boost performance for free - just by reordering your fields to minimize padding? This simple yet powerful optimization can save tens of megabytes in large-scale applications without writing a single extra line of code.",7,[10,11,12],"golang","optimization","compiler",484,"high-performance-golang-struct-optimizations-paddings-and-alignments","Golang struct optimizations: Paddings and Alignments.","2025-06-16T08:48:45.460394Z",true]