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 Jump Into A Hole Stickers - Find & Share on GIPHY


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

go
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

go
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

go
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.source and you can even reassign shunk1.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 type keyword or giving the struct type a name but we will hold it in a variable directly

two ways to declare anonymous structs

go
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

go
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

go
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

go
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

go
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 float64 must 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-byte float64), 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

go
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{}{} and ok = true
  • if key missing → returns zero-value of struct which is struct{}{} but ok = 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 Peace Out GIFs | Tenor