Hi everyone! this is Jimmy , and this is the third article in my series “Breaking Things with Go.” In this series, I document my journey through Jon Bodner’s Second Edition: Learning Go – An Idiomatic Approach to Real-World Go Programming and explore how to use Go in the most practical way I can
in this series the resources are the book itself, go documentation, and any AI model to clarify some things
lets Jump into it

in the last Break we talked about Maps and how are they useful in Go
but they have a limitations, one of those limitations that the keys must be from the same type so what if i need to create like a JSON API request and response where the fields might be different and that’s when structs come in handy
you should take a look at this after reading this break memory Alignment for more info about that topic
Introduction to structs
- structs defined inside a function are only used inside that (and i mean only inside it = pointers can’t help, can’t return, can’t do nothing)
- you can declare them at package level though and they will be accessible through multiple functions
- unlike maps, there is no nil structs so whether you declare struct without assignment or you declare and assign empty struct → both will assign zero values of the struct type
there is multiple ways to declare structs in Go
first using type keyword
type data struct {
source string
destination string
size int
} // no need for commas to separate fields just each in a line
- Fields are stored in declaration order (top to bottom)
- Padding is added for alignment (follows platform's word size) and we will get to that later
- Memory is contiguous for all fields (and that’s what causes padding “i think”)
- empty structs (with no fields) is zero-sized (doesn’t cost memory at all)
to create an instance of that struct and assign values there is multiple ways first using positional style
shunk1 := data{
"192.164.242.2",
"192.164.242.159",
12312414,
} // you have to use commas at assignment unlike declaration
second using map literal style
shunk1 := data{
source: "192.164.242.2",
size: 12312414,
} // you have to use commas at assignment unlike declaration
here are the differences and some notes
- in the positional style you have to assign a value for each field, but in the map literal style you can assign what you need only and the rest will be set to its field zero value
- positional style you have to care for the assignment order unlike the map literal cause you already specify the field name
- you can’t mix two ways, you either use this or that
- you can access the fields in struct using dot notation
shunk1.sourceand you can even reassignshunk1.size=123
as you can see at this way of declaration we can create multiple instances from the same struct but what if we need to use the struct on the fly ? then we can use whats known as anonymous struct
Anonymous structs
anonymous struct is struct implementation without using the
typekeyword or giving the struct type a name but we will hold it in a variable directly
two ways to declare anonymous structs
var data struct { //notice we created a variable of struct not a type
source string
destination string
size int
}
//then we will assign values using dot notation
data.source = "192.164.242.2"
data.destination= "192.164.242.159"
data.size= 12312414
or we can do it this way
data := struct { //notice we created a variable of struct not a type
source string
destination string
size int
}{
"192.164.242.2",
"192.164.242.159",
12312414,
}
anonymous structs are just single instances of that struct to be used on the fly
Comparing structs
structs are generally comparable types ==(functions, and channel fields prevent struct from being comparable)==
in Go, Types are identified by their name, not just the shape
so different Types aren’t comparable even if they have the same fields’ structure but we can convert between them (only if they have the same structure)
same structure meaning same fields’ names, same data types, and same order
take a look at this
func main() {
type fPerson struct {
name string
age int
}
type sPerson struct {
name string
age int
}
a := fPerson{
"Ali",
30,
}
b := sPerson(a)
fmt.Println(b) // {Ali 30}
fmt.Println(reflect.TypeOf(b)) // main.sPerson
fmt.Println(a == b) // compiling error: mismatched types fPerson and sPerson
fmt.Println(a == fPerson(b)) // true
}
in that line a == fPerson(b) we explicitly converted b to a’s type
these rules kinda change with anonymous structs (so there is no types just casual variable structs so they are the same type) → meaning they can be compared without conversion if they have the same structure
type firstPerson struct {
name string
age int
}
f := firstPerson{
"bob",
40,
}
var g struct {
name string
age int
}
g = f
fmt.Println(f == g) //true
Memory Alignment
the struct type fields’ order affects the memory efficiency in Go and here is how
type Efficient struct {
value float64 // 8 bytes
name string // 16 bytes = 8 pointer + 8 length
isActive bool // 1 byte
} //Total: 24 bytes
type Inefficient struct {
isActive bool // 1 byte
value float64 // 8 bytes - needs 7 bytes padding after bool
name string // 16 bytes
} // Total: 32 bytes
and i think this happens because as we mentioned above Memory is contiguous for all fields in GO so it has to add padding
- Field Alignment: In Go, every field must start at a memory address that is a multiple of its own "natural alignment" (usually its size). For example, on a 64-bit system, an 8-byte
float64must start at an address divisible by 8 - Internal Padding: If a small field (like a 1-byte
bool) is followed by a larger field (like an 8-bytefloat64), the compiler inserts internal padding bytes after the small field. This "pushes" the larger field to the next properly aligned memory address - Struct Alignment & Trailing Padding: The entire struct itself must have an alignment equal to its largest field's alignment requirement. To ensure this, the compiler adds trailing padding at the end so the total size of the struct is a multiple of that largest alignment
Using Structs in Sets implementation
you remember how we mentioned using maps as sets in the last break, well there is down side for this and let me explain
do you remember, when we said that values doesn’t matter in sets implementation we are just playing around the uniqueness of keys in maps cause it will make sure there is no duplicated values
now why would we implement that with bools that will take 1 byte when we can use zero-sized empty structs as value (value doesn’t matter anyway)
and that’s how we do it
intSet := map[int]struct{}{}
vals := []int{5, 10, 2, 5, 8, 7, 3, 9, 1, 2, 10}
for _, v := range vals {
intSet[v] = struct{}{} // keys are unique so it will store each value once only
}
if _, ok := intSet[5]; ok {
fmt.Println("5 is in the set")
}
now of course we will have to use the comma-ok idiom while checking value existence because
- if key exists → returns stored
struct{}{}andok = true - if key missing → returns zero-value of struct which is
struct{}{}butok = false
so without the ok we won’t be able to tell if it is zero-value struct or actual stored empty-struct
Takes
- Structs can be declared at function or package level, but only package-level structs can be used across multiple functions
- Memory layout and efficiency matter: field order affects struct size
- Anonymous structs are single-use instances
- Structs can be used for sets with zero-sized values
Coming Next
the next break will be about block Blocks and some controlling statements
so stick around for next break where we will break more stuff
feel free to reach out to me
