07. Structs and Methods
🚀 Master Go's structs and methods! Learn how to define structs, leverage embedded fields, understand method sets, and use struct tags effectively. This post empowers you to build robust and organized Go applications. ✨
What we will learn in this post?
- 👉 Defining Structs
- 👉 Anonymous Structs and Embedded Fields
- 👉 Methods in Go
- 👉 Pointer vs Value Receivers
- 👉 Method Sets
- 👉 Struct Tags
- 👉 Conclusion!
Structs in Go 🚀
Go structs are like blueprints for creating custom data types. They bundle together different fields, each with its own type.
Defining Structs 📝
To define a struct, we use the type keyword followed by the struct’s name and the struct keyword, and then list the fields and their types within curly braces:
1
2
3
4
type Person struct {
Name string
Age int
}
Here, Person is a struct with two fields: Name (a string) and Age (an integer).
graph LR
S_DECL["Struct Declaration"]:::javaStyle --> S_FIELD["Fields: Name, Age"]:::jdbcStyle
S_FIELD --> S_INST["Instance Creation"]:::driverStyle
S_INST --> S_USE["Use Struct"]:::useStyle
classDef javaStyle fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef jdbcStyle fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef driverStyle fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef useStyle fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class S_DECL javaStyle;
class S_FIELD jdbcStyle;
class S_INST driverStyle;
class S_USE useStyle;
linkStyle default stroke:#e67e22,stroke-width:3px;
Initializing Structs ✨
We can initialize structs in a few ways:
- Struct Literals:
1
person1 := Person{"Alice", 30} // Order matters!
- Field: Value Syntax:
1
person2 := Person{Name: "Bob", Age: 25} // Order doesn't matter!
new()Function:
1
2
3
person3 := new(Person) // Returns a pointer to a zero-valued Person
person3.Name = "Charlie"
person3.Age = 40
Nested Structs 🏠
Structs can contain other structs as fields, creating nested structures:
1
2
3
4
5
6
7
8
9
10
11
type Address struct {
Street string
City string
}
type Employee struct {
Name string
Address Address // Nested struct
}
employee1 := Employee{Name: "David", Address: Address{Street: "123 Main St", City: "Anytown"}}
graph LR
EMP["Employee Struct"]:::javaStyle --> EMP_NAME["Name Field"]:::jdbcStyle
EMP --> EMP_ADDR["Address Field"]:::driverStyle
EMP_ADDR --> ADDR["Address Struct"]:::dbStyle
ADDR --> ADDR_STREET["Street Field"]:::jdbcStyle
ADDR --> ADDR_CITY["City Field"]:::jdbcStyle
classDef javaStyle fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef jdbcStyle fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef driverStyle fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef dbStyle fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class EMP javaStyle;
class EMP_NAME jdbcStyle;
class EMP_ADDR driverStyle;
class ADDR dbStyle;
class ADDR_STREET jdbcStyle;
class ADDR_CITY jdbcStyle;
linkStyle default stroke:#e67e22,stroke-width:3px;
- More details and advanced struct concepts can be found in the official Go documentation.
- Tutorialspoint also provides a good resource for structs Tutorialspoint
Structs in Go: One-Time Use & Composition 🧩
#
| Feature | Struct Embedding (Composition) | Inheritance (Classical OOP) |
|---|---|---|
| Relationship | “Has-a” (contains another type) | “Is-a” (inherits from another type) |
| Supported in Go? | Yes | No |
| Field Promotion | Yes (fields promoted to outer type) | No (fields accessed via parent class) |
| Code Reuse | By embedding | By inheritance |
| Flexibility | High (can embed multiple types) | Lower (single inheritance chain) |
| Coupling | Loose | Tight |
Let’s explore structs in Go, focusing on anonymous structs for one-off situations and struct embedding for building complex types.
Anonymous Structs 👻
| Feature | Anonymous Structs | Named Structs |
|---|---|---|
| Definition | Defined inline, no type name | Defined with a type name |
| Usage Scope | One-off, temporary | Reusable, used in multiple places |
| Syntax | var x = struct { ... }{ ... } | type X struct { ... } |
| Typical Use Case | Quick grouping, function params | Data models, application logic |
| Readability | Lower for complex structs | Higher, especially for large structs |
Anonymous structs are defined without a name and are useful when you need a struct type only once.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
person := struct {
Name string
Age int
}{
Name: "Alice",
Age: 30,
}
fmt.Println(person) // Output: {Alice 30}
}
Struct Embedding: Composition over Inheritance 🏗️
Go doesn’t have inheritance in the traditional sense. Instead, it uses struct embedding which provides a powerful composition mechanism.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import "fmt"
type Address struct {
Street string
City string
}
type Employee struct {
ID int
Address // Embedded field
}
func main() {
emp := Employee{
ID: 123,
Address: Address{
Street: "1 Main St",
City: "Anytown",
},
}
fmt.Println(emp.Street) // **Promoted field!** Output: 1 Main St
fmt.Println(emp.Address.City) //Output: Anytown
}
- Embedded Fields:
Addressis embedded inEmployee. - Promotion: Fields from the embedded struct are promoted to the outer struct. So
emp.Streetdirectly accesses theStreetfield of the embeddedAddressstruct.
Composition vs. Inheritance ⚖️
graph LR
EMP_E["Employee Struct"]:::javaStyle --> ADDR_E["Embedded Address Struct"]:::dbStyle
ADDR_E --> ADDR_STREET_E["Street"]:::jdbcStyle
ADDR_E --> ADDR_CITY_E["City"]:::jdbcStyle
EMP_E --> EMP_ID["ID Field"]:::driverStyle
EMP_E --> PROMO["Promoted: emp.Street"]:::useStyle
classDef javaStyle fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef jdbcStyle fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef driverStyle fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef dbStyle fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef useStyle fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
class EMP_E javaStyle;
class ADDR_E dbStyle;
class ADDR_STREET_E jdbcStyle;
class ADDR_CITY_E jdbcStyle;
class EMP_ID driverStyle;
class PROMO useStyle;
linkStyle default stroke:#e67e22,stroke-width:3px;
- Composition: Creates a “has-a” relationship.
Employeehas anAddress. It’s flexible and avoids the tight coupling of inheritance. - Inheritance: Creates an “is-a” relationship. Not directly supported in Go. Composition provides the benefits of code reuse without the complexities of inheritance hierarchies.
Instead of thinking of Employee inheriting from Address, think of Employee containing an Address and conveniently providing direct access to its fields. Check out this awesome blog to know more.
Methods on Structs in Go ⚙️
Go lets you add behaviors to your struct types using methods. Think of it like giving your data abilities!
Defining Methods with Receiver Functions 🤝
The magic happens with a receiver function. Here’s how it looks:
1
2
3
func (r MyStruct) MyMethod() {
// Your code here, accessing fields of 'r'
}
func: Keyword for defining a function.(r MyStruct): The receiver.ris a variable (likethisorselfin other languages) that refers to an instance ofMyStruct.MyMethod(): The name of your method.
Calling Methods and Chaining 📞🔗
You call methods using the . operator:
1
2
myVar := MyStruct{ /* ... */ }
myVar.MyMethod()
If a method returns a value that allows you to call another method on it directly, you can use method chaining:
1
result := myVar.Method1().Method2().Method3()
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Rectangle struct {
Width int
Height int
}
func (r Rectangle) Area() int {
return r.Width * r.Height
}
func main() {
myRect := Rectangle{Width: 10, Height: 5}
area := myRect.Area() // calling the Area method.
println(area) // output is 50
}
Pointer vs. Value Receivers in Go 🛠️
In Go, methods can have either pointer receivers (*T) or value receivers (T). This choice impacts how the method interacts with the underlying data. Let’s break it down!
Understanding the Difference 🧐
Value Receiver (T): The method operates on a copy of the original struct. Changes made inside the method do not affect the original struct. Think of it like making a photocopy and editing it. 📝
Pointer Receiver (*T): The method operates directly on the original struct. Changes made inside the method do affect the original struct. Think of it like directly editing a document. 💻
| Feature | Value Receiver (T) | Pointer Receiver (*T) |
|---|---|---|
| Operates On | Copy of struct | Original struct (reference) |
| Can Modify Original? | No | Yes |
| Use Case | Read-only methods | Methods that modify struct fields |
| Performance | May be costly for large structs | Efficient for large structs |
| Interface Satisfaction | Satisfies interface with value | Satisfies interface with pointer |
| Method Call | obj.Method() | (&obj).Method() or obj.Method() |
When to Use Which? 🤔
- Mutation (Changing Data): Use a pointer receiver (
*T) if the method needs to modify the struct’s fields. If we want to save the changes. ✅ - Read-Only (No Changes): Use a value receiver (
T) if the method only needs to read the struct’s fields and doesn’t modify them. 📖 - Large Structs (Performance): Use a pointer receiver (
*T) for large structs to avoid copying the entire struct every time the method is called. This can significantly improve performance. 🚀
Go’s Magic: Automatic Conversions ✨
Go automatically handles conversions. You can call a method with a pointer receiver on a value, and Go will automatically take the address. Similarly, you can call a method with a value receiver on a pointer, and Go will automatically dereference it.
Comparative Examples 💡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Circle struct {
Radius int
}
// Value receiver - Doesn't modify the original.
func (c Circle) AreaValue() int {
c.Radius = 100 // This DOES NOT affect the original circle
return c.Radius * c.Radius
}
// Pointer receiver - Modifies the original.
func (c *Circle) DoubleRadius() {
c.Radius = c.Radius * 2 // This affects the original circle.
}
func main() {
myCircle := Circle{Radius: 5}
area := myCircle.AreaValue() //Calls area value on value reciever
println("Area:",area) //prints 10000 since value reciever works on a copy.
myCircle.DoubleRadius()
println("Radius:", myCircle.Radius) //prints Radius 10
}
Summary
Value RecieverUse if you only have to read the data.Pointer RecieverUse if you have to read and modify the data.
Here’s an explanation of method sets in Go, designed to be friendly and easy to understand!
Method Sets in Go: 🧐 What Are They?
Method sets determine which methods a type has. This is super important for interfaces!
Value vs. Pointer Receivers
- A value receiver gets a copy of the value. Both a
Tand*T(pointer toT) can call these methods. - A pointer receiver gets the actual value’s address. Only
*Tcan directly call these methods. ATcan call it if Go can automatically get its address.
Interface Satisfaction and Method Sets
A type satisfies an interface if its method set includes all the interface’s methods.
- If an interface has pointer receiver methods, only a
*Tcan satisfy it. - If an interface has value receiver methods, both
Tand*Tcan satisfy it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type MyInterface interface {
MethodWithPointerReceiver()
}
type MyType struct {}
func (t *MyType) MethodWithPointerReceiver() {} // *MyType implements MyInterface
func main() {
var i MyInterface
mt := MyType{}
i = &mt // Correct!
//i = mt // Error: MyType does not implement MyInterface (MethodWithPointerReceiver has pointer receiver)
}
Example: Seeing it in Action 🚀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Number int
// Value receiver
func (n Number) String() string {
return fmt.Sprintf("Value: %d", n)
}
// Pointer receiver
func (n *Number) Increment() {
*n++
}
func main() {
num := Number(5)
fmt.Println(num.String()) // Works: Value receiver can be called on value
(&num).Increment() // Works: Pointer receiver called on pointer
fmt.Println(num.String())
}
In the example:
String()is in the method set of bothNumberand*Number.Increment()is only in the method set of*Number.- More Info: Check out this article about method sets. Go Method Sets
Here’s a friendly explanation of struct tags in Go:
Struct Tags Explained 🏷️
Struct tags are little pieces of metadata you can add to the fields of a struct in Go. They’re written inside backticks ` after the field’s type. Think of them as instructions for tools like JSON encoders, validators, or database mappers.
What are they for? 🤔
JSON Marshaling (
json:"name"): Tells thejsonpackage how to name a field when encoding to JSON. For example:FieldName stringjson:”field_name”will make the JSON keyfield_name`.Validation: Libraries can use tags to define validation rules.
Name stringvalidate:”required,min=3”might specify thatName` must not be empty and have at least 3 characters.Database ORM Mapping: Help ORMs (Object-Relational Mappers) map struct fields to database columns.
ID intdb:”user_id,primarykey”could indicate that theIDfield corresponds to theuser_id` column and is a primary key.
How to Use Them? 🚀
1
2
3
4
type User struct {
ID int `json:"id" db:"user_id,primarykey"`
Name string `json:"name" validate:"required"`
}
Syntax:
FieldName Typetag1:”value1” tag2:”value2”Accessing Tags: You need to use reflection to read tag values at runtime:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name,omitempty" validate:"required"`
Age int `json:"age"`
}
func main() {
personType := reflect.TypeOf(Person{})
for i := 0; i < personType.NumField(); i++ {
field := personType.Field(i)
jsonTag := field.Tag.Get("json")
validateTag := field.Tag.Get("validate")
fmt.Printf("Field: %s, JSON Tag: %s, Validate Tag: %s\n", field.Name, jsonTag, validateTag)
}
}
This code snippet shows how to inspect the tags of a struct’s fields. The reflect package is used to get the type information of the struct and then iterate over its fields. For each field, the Tag.Get method is used to retrieve the value of the json and validate tags. Reflection in Go
Practical example 💡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"user_name"`
Email string `json:"email,omitempty"` // omitempty: will hide if email is ""
passwd string `json:"-"` //-: will hide password
}
func main() {
user := User{ID: 1, Name: "John Doe", Email: "john.doe@example.com", passwd: "securePassword"}
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Error marshaling JSON:", err)
return
}
fmt.Println(string(jsonData))
}
output:
1
{ "id": 1, "user_name": "John Doe", "email": "john.doe@example.com" }
Here, the Name Field in Json will be user_name, Email will hide when empty and passwd will hide.
Tags offer a powerful way to customize how Go structs interact with external systems and libraries, making your code more flexible and expressive.
Option 3:
Conclusion
Alright, we’ve reached the end! 😊 I’m really keen to know what you think. Any comments, questions, or suggestions? Please share them below! Your input helps make this even better. 👇 Looking forward to reading them! 🚀