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

Types

Types can be declared at any block level (package down), but are only accessible within their scope (except exported types from other packages) and there is two terms that you should be familiar with

  • Abstract type → Specifies what a type should do but not how
  • Concrete type → Specifies what and how with a data storage and methods implementations

we already dealt with types before so I'll just start with Methods and if there is something we need to know more about types we'll be said on the fly

Methods

Basic Method Declaration

Methods are defined at the package block level with a receiver specification:

go
type Person struct {
    FirstName string
    LastName  string
    Age       int
}

func (p Person) String() string {
    return fmt.Sprintf("%s %s, age %d", p.FirstName, p.LastName, p.Age)
}
  • Receiver appears between func keyword and method name
  • Receiver name is conventionally a short abbreviation (usually first letter)
  • Don't use this or self (nonidiomatic, we need a descriptive name)
  • Methods must be defined at package block level (unlike functions)
  • Method names cannot be overloaded
  • Methods must be in the same package as their type

and there is two types of receivers and each one has its own use case

  1. pointer receiver
    • Method modifies the receiver
    • Method needs to handle nil instances
    • Other methods on the type user pointer receiver to keep consistency
  2. value receiver
    • Method doesn't modify receiver
    • following consistency with other value receiver method
go
type Counter struct {
    total       int
    lastUpdated time.Time
}

func (c *Counter) Increment() {  // pointer receiver that will modify counter instance
    c.total++
    c.lastUpdated = time.Now()
}

func (c Counter) String() string {  // value receiver that will read the counter instance only  
    return fmt.Sprintf("total: %d, last updated: %v", c.total, c.lastUpdated)
}

Go provides what's known as syntactic sugar for method calls:

  • Value variable calling pointer method: c.Increment()(&c).Increment()
  • Pointer variable calling value method: c.String()(*c).String()

but the method set is kind different

  • Pointer instance: Can call both pointer and value receiver methods
  • Value instance: Can only call value receiver methods

and the standard rules apply here

go
func doUpdateWrong(c Counter) {  // Receives copy
    c.Increment()  // Modifies copy, not original
}

func doUpdateRight(c *Counter) {  // Receives pointer
    c.Increment()  // Modifies original
}

and we can assign the method value to variable

go
type Adder struct {
    start int
}

func (a Adder) AddTo(val int) int {
    return a.start + val
}

myAdder := Adder{start: 10}
f1 := myAdder.AddTo      // Method value
fmt.Println(f1(10))      // prints 20

or we can create a function from the type itself

go
f2 := Adder.AddTo                // Method expression
fmt.Println(f2(myAdder, 15))     // prints 25
// Signature: func(Adder, int) int

Functions vs methods

so when to use which ? Use methods when:

  • Logic depends on configured or mutable state
  • Data should be stored in a struct

Use functions when:

  • Logic depends only on input parameters
  • No external state needed

Iota for enumeration

  • Go doesn't have a traditional enumeration type
  • Instead, it uses iota to assign increasing values to constants
  • The concept comes from APL (A Programming Language)

for example

go
type MailCategory int

const (
    Uncategorized MailCategory = iota  // 0
    Personal                            // 1
    Spam                                // 2
    Social                              // 3
    Advertisements                      // 4
)
  • iota starts at 0 and increments for each constant in the block
  • Resets to 0 when a new const block is created
  • Type and assignment are automatically repeated for subsequent constants
  • Increments even when not explicitly used
  • it doesn't have to increment by 1 you can do operation like this Uncategorized MailCategory = 1 << iota to shift the number binary wise to get 1,2,4,8,16, etc..
  • we can also start from a specific number like this Uncategorized MailCategory = iota+number and this (iota+number) expression is repeated for the entire block anyway

when to use it ?

  • Use: For internal constants where values are referred to by name, not value
  • Avoid: When values are explicitly defined in specifications or represent external system values
  • Risk: Inserting constants in the middle renumbers all subsequent values

and we can skip numbers

go
const (
    _ MailCategory = iota  // skip 0
    Personal               // 1
    Spam                   // 2
)

Embedding for Compositions

go
type Employee struct {
    Name string
    ID   string
}

type Manager struct {
    Employee              // embedded field
    Reports []Employee
}
  • Fields and methods from embedded types are promoted to the containing struct
  • Can be accessed directly: manager.ID instead of manager.Employee.ID
  • Any type can be embedded, not just structs

just note that When names clash, use the embedded type explicitly:

go
type Outer struct {
    Inner
    X int  // shadows Inner.X
}
o.X        // accesses Outer.X
o.Inner.X  // accesses Inner.X

Interfaces

go
type Stringer interface {
    String() string
}

Interfaces typically end in "er": Stringer, Reader, Closer, Handler

  • No explicit declaration needed
  • If a type has all methods in an interface's method set, it implements that interface
  • Enables both type safety and decoupling

and here is an example of interfaces

real-world example

Now this is important

I Understood this chapter quite good when i first studied it but I didn't work on that much of applications or scripts that uses methods and interfaces so I got a little rusty before writing this and this is totally fine and the issue with this that it makes every thing static like use this not that and in actual scenario it doesn't work that way it is always dynamic and it is important to decide before starting how will you implement things

with that being said this chapter is done

Takes

  1. Methods are just functions with a receiver
  2. Pointer receivers vs value receivers matter
  3. Interfaces are implicit → You never declare that a type "implements"
  4. Interfaces define behavior, not data
  5. The empty interface accepts anything

Coming Next

the next break will go through Generics in Go so stick around feel free to reach out to me Peace Out GIFs | Tenor