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:
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
funckeyword and method name - Receiver name is conventionally a short abbreviation (usually first letter)
- Don't use
thisorself(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
- pointer receiver
- Method modifies the receiver
- Method needs to handle nil instances
- Other methods on the type user pointer receiver to keep consistency
- value receiver
- Method doesn't modify receiver
- following consistency with other value receiver method
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
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
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
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
iotato assign increasing values to constants - The concept comes from APL (A Programming Language)
for example
type MailCategory int
const (
Uncategorized MailCategory = iota // 0
Personal // 1
Spam // 2
Social // 3
Advertisements // 4
)
iotastarts 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 << iotato 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+numberand 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
const (
_ MailCategory = iota // skip 0
Personal // 1
Spam // 2
)
Embedding for Compositions
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.IDinstead ofmanager.Employee.ID - Any type can be embedded, not just structs
just note that When names clash, use the embedded type explicitly:
type Outer struct {
Inner
X int // shadows Inner.X
}
o.X // accesses Outer.X
o.Inner.X // accesses Inner.X
Interfaces
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
package main
import (
"fmt"
"math"
)
type Circle struct {
raduis float64
}
type Rectangle struct {
length float64
width float64
}
type Shape interface {
area() float64
}
func (c Circle) area() float64 {
return math.Pi * math.Pow(c.raduis, 2)
}
func (r Rectangle) area() float64 {
return r.length * r.width
}
func getArea(s Shape) float64 {
return s.area()
}
func main() {
c1 := Circle{28.2}
r1 := Rectangle{29.1, 20.3}
// here i need to loop over all shapes' areas no matter how much they are but without calling area for each so we do this
shapes := []Shape{c1, r1}
for _, v := range shapes {
fmt.Println(getArea(v))
}
}
real-world example
package main
import "fmt"
type Priority int
const (
Low Priority = iota
Medium
High
)
type Notification struct {
Title string
Message string
Priority Priority
}
type Sender interface {
Send(n Notification)
}
type EmailSender struct{}
func (e EmailSender) Send(n Notification) {
fmt.Println("Sending Email: " + n.Title)
}
type SMSSender struct{}
func (s SMSSender) Send(n Notification) {
if n.Priority != High {
return // SMS only sends high priority notifications
}
fmt.Println("Sending SMS: " + n.Message)
}
type TestSender struct {
Messages []string
}
func (t *TestSender) Send(n Notification) {
t.Messages = append(t.Messages, n.Message)
}
func Notify(s Sender, n Notification) {
s.Send(n)
}
func main() {
n1 := Notification{
Title: "First Message",
Message: "Hello There",
Priority: Medium,
}
Notify(SMSSender{}, n1) // skipped not High priority
Notify(EmailSender{}, n1) // prints email
t := &TestSender{} // must use pointer pointer receiver
Notify(t, n1)
fmt.Println(t.Messages)
}
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
- Methods are just functions with a receiver
- Pointer receivers vs value receivers matter
- Interfaces are implicit → You never declare that a type "implements"
- Interfaces define behavior, not data
- 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
