This episode dissects data modeling in Go: structs as data models with field tags and method receivers, interfaces with idiomatic duck typing, plus modern generics with type constraints, generic slices and maps, and reusable algorithms.

A program full of separate variables quickly becomes chaotic. That's where structs and interfaces come in: structs shape data models, and interfaces define behavior contracts. Combined with generics, stable since Go 1.18, you can write reusable algorithms without losing type safety.
Episode 5 covers all three elements in depth: structs with field tags and method receivers, interfaces with duck typing, and modern generics with constraints and generic slices/maps. This is the data modeling toolkit you will use in every episode that follows.
A struct is a collection of named fields with specific types. Field tags are metadata strings behind the field type — the most common tags are used for JSON encoding:
package main
import (
"encoding/json"
"fmt"
)
type Pengguna struct {
Nama string `json:"nama"`
Umur int `json:"umur"`
}
func main() {
u := Pengguna{Nama: "Arman", Umur: 30}
data, _ := json.Marshal(u)
fmt.Println(string(data))
}The json:"nama" tag controls the field name when marshaling and unmarshaling. The omitempty option omits empty fields: json:"umur,omitempty". The same tag convention is used by other libraries, such as ORMs, for database columns. There are three ways to create a struct instance: the keyed literal Pengguna{Nama: "Arman"}, the positional literal, and new. The keyed literal is recommended because it is explicit.
A method is a function with a receiver — the type that owns the method. The pointer receiver *Pengguna allows a method to modify fields:
type Pengguna struct {
Nama string
Umur int
}
func (p *Pengguna) BertambahUmur() {
p.Umur++
}The rule is consistent: if one method uses a pointer receiver, all methods on that type should use a pointer receiver. Mixed usage is confusing and triggers errors like "cannot use value as pointer".
An interface is a collection of methods. A type implicitly satisfies an interface if it has all the declared methods — no implements keyword. This is the essence of idiomatic Go duck typing: as long as it behaves like a duck, it's a duck.
package main
import "fmt"
type Pengguna struct {
Nama string
Umur int
}
func (p Pengguna) String() string {
return fmt.Sprintf("%s berumur %d", p.Nama, p.Umur)
}
func main() {
var s fmt.Stringer = Pengguna{Nama: "Arman", Umur: 30}
fmt.Println(s)
}By implementing String(), the Pengguna type automatically satisfies the fmt.Stringer interface from the standard library, and fmt.Println uses that representation.
Before generics, interface{} was used for any type. Since Go 1.18, its official alias is any. Be careful: any defers type checking to runtime and hands it over to the user via type assertions:
func cetakNilai(v any) {
if angka, ok := v.(int); ok {
fmt.Println("integer:", angka)
}
}Generics let a function work with many types without duplication. Type parameters are written in square brackets, and a constraint limits which types are allowed.
package main
import "fmt"
func CetakDua[T any](a, b T) {
fmt.Println(a, b)
}
func main() {
CetakDua(1, 2)
CetakDua("satu", "dua")
}The standard library provides the comparable constraint for types that support ==. For specific needs, you can define your own constraints:
type Numerik interface {
~int | ~float64
}
func RataRata[T Numerik](data []T) T {
var total T
for _, v := range data {
total += v
}
return total / T(len(data))
}The ~ sign allows alias types with the same underlying representation. The slices and maps packages, stable since Go 1.21, use generics for common operations such as slices.Sort, slices.Contains, and maps.Keys — no more writing sort helpers for every type. To see the list of available functions, run go doc slices from your terminal.
In real applications, all three work together. Here's an example of a generic repository:
package main
type Penyimpan[T any] struct {
items []T
}
func (s *Penyimpan[T]) Tambah(item T) {
s.items = append(s.items, item)
}
func (s *Penyimpan[T]) Semua() []T {
return s.items
}This pattern shows the power of Go: structs carry state, method receivers define behavior, and generics make it reusable for any type. This is the foundation of the repository pattern you will apply when connecting applications to a database in episode 8.
Episode 5 completed your data modeling toolkit: structs with field tags for serialization and method receivers for behavior, interfaces with implicit duck typing and any, and modern generics with type parameters, custom constraints, and the generic slices and maps packages.
Key takeaways:
any is the alias for interface{} meaning any type.slices and maps packages provide standard generic algorithms.In the next episode we will discuss error handling, testing, and basic debugging — error handling idioms with error, fmt.Errorf, and errors.Is or errors.As, unit testing with the testing package, table-driven tests, and benchmarking with go test. Your code quality will be measured, not just assumed.