Struct is a user-defined type that groups together fields (variables) of different types.
package main
import "fmt"
type gasEngine struct {
mpg uint8
gallons uint8
ownerInfo owner // you can also just type owner and you would have name owner
}
type owner struct {
name string
}
func main() {
var myEngine gasEngine = gasEngine{25, 15, owner{"Alex"}}
fmt.Println(myEngine.mpg, myEngine.gallons, myEngine.ownerInfo.name)
}
Structs can also be defined like this but they wouldn’t be reusable in this case.
package main
import "fmt"
func main() {
var myEngine = struct {
mpg uint8
gallons uint8
}{25, 15}
fmt.Println(myEngine.mpg, myEngine.gallons)
}
There's also mamed field struct literals which assign values using explicit field names (e.g. Rectangle{Width: 12, Height: 6}), which makes them safer and more readable than positional struct literals that rely on field order (e.g. Rectangle{12, 6}); in tests, named fields are preferred because they prevent silent bugs when struct definitions change and improve clarity and maintainability.
// Named fields (safe, explicit, recommended in tests)
type Rectangle struct {
Width float64
Height float64
}
r1 := Rectangle{Width: 12, Height: 6}
// Positional fields (order-dependent, more fragile)
r2 := Rectangle{12, 6}
Use structs when you need to group related fields of different types into a single entity, like modeling a User with name, email, and age so you pass one clean object instead of scattered variables.
Embedding allows one struct type to include another struct, inheriting the fields and methods of the embedded type. It is Go's mechanism to achieve composition over traditional inheritance.
type Address struct {
Street, City, State string
}
type Person struct {
Name string
Address
}
p := Person{
Name: "Alice",
Address: Address{"123 Main St", "Anytown", "CA"},
}
fmt.Println(p.Street) // Output: 123 Main St
Embedding provides a way to "inherit" methods. If the embedded type has methods, the embedding type will have those methods too, provided it doesn't define its own methods with the same name.
func (a Address) FullAddress() string {
return a.Street + ", " + a.City + ", " + a.State
}
// Even though Person doesn't define FullAddress, it gains the method through embedding Address.
address := p.FullAddress()
However, Go doesn't support classical inheritance where you can extend and override base class methods. Instead, Go promotes composition over inheritance, making systems easier to understand and maintain.
Methods are functions that are associated with a specific type (a function with a receiver), such as a struct. They allow you to define behavior for instances of that type. Methods are similar to functions but have a receiver, which ties them to a particular type.
A receiver is a parameter of a method that binds the method to a specific type. This is similar to what is commonly known as “this” or “self” in other programming languages.
Receivers can be applied to both value and pointer types.
Value receiver is a method that operates on a copy of the instance of the associated type. When you define a method with a value receiver, any modifications made to the receiver inside the method will not affect the original instance.
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.radius * c.radius
}
Pointer receiver is a method that operates directly on the instance of the associated type. When you define a method with a pointer receiver, any modifications made to the receiver inside the method will directly affect the original instance.
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
Use value receivers for read-only methods that don't modify the struct (for example, calculating a user's age from stored data), and use pointer receivers when the method must update the original struct (for example, incrementing a user's login count or changing account settings).
Use pointer receivers when the method needs to modify the instance’s state directly. Pointer receivers are essential when you want to modify the underlying state of a struct or any other type.
Method declaration: func (receiverName ReceiverType) MethodName(args). Meaning the this function belongs to the type ReceiverType.
A method declaration binds an identifier, the method name, to a method, and associates the method with the receiver's base type.
package main
import "fmt"
// Define a struct
type Rectangle struct {
Width float64
Height float64
}
// Define a method for the Rectangle struct
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Define another method with a pointer receiver
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
// Create an instance of Rectangle
rect := Rectangle{Width: 10, Height: 5}
// Call the Area method
fmt.Println("Area:", rect.Area()) // Output: Area: 50
// Call the Scale method
rect.Scale(2)
fmt.Println("Scaled Width:", rect.Width) // Output: Scaled Width: 20
fmt.Println("Scaled Height:", rect.Height) // Output: Scaled Height: 10
}
Use methods when you want to attach behavior directly to a type, like adding a FullName() method to a User struct so the logic lives with the data rather than floating in unrelated functions.
Go does not support function overloading (where you can have two functions of the same name but with different argument type) because Go identifies functions by name only, not by parameter types. However, functions with the same name can exist in different packages:
// Package rectangle
func Area(r Rectangle) float64 {
return r.Width * r.Height
}
// Package circle
func Area(c Circle) float64 {
return math.Pi * c.Radius * c.Radius
}
// Then you would call them as:
rectangle.Area(rect)
circle.Area(c)
This solution can be overkill, for the example above. Instead, use methods. Methods are attached to different receiver types, so there's no naming conflict.
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Now both types can have an Area method:
rect.Area()
circle.Area()
Take note that there is NO magical behavior:
// This:
func (c Circle) Area() float64
// Is just syntactic sugar for:
func Area(c Circle) float64
circle.Area() instead of Area(circle).It is a convention in Go to have the receiver variable be the first letter of the type.
r Rectangle
Methods with value receivers operate on a copy of the receiver value. Changes made inside the method do not affect the original value. Use a pointer receiver (Type) when the method needs to modify the original value or avoid copying large structs.
Go lets you create new types from existing ones (type aliasing) using type NewName OriginalType.
package main
import "fmt"
type Age int
func main() {
var a Age = 21
fmt.Println(a)
}
Age is a new type based on int, so it behaves like an int but is treated as a distinct type by the compiler. This improves type safety because Age and int are not interchangeable without explicit conversion.
You can declare methods on custom types just like on structs, which is useful for adding domain-specific behavior on top of primitive types (e.g., adding a Deposit method to type Bitcoin int).
The main benefit is type safety: even though type Bitcoin int and type AccountID int are both int under the hood, the compiler treats them as distinct types, so passing a Bitcoin where an AccountID is expected won't compile, catching mistakes that plain int would silently allow.
Implement the fmt.Stringer interface by defining a String() string method on your type to control how it is printed with %s (e.g., func (b Bitcoin) String() string { return fmt.Sprintf("%d BTC", b) } prints 10 BTC instead of 10).
// Imagine you have a function:
func Transfer(from int, to int, amount int) {}
//Nothing stops you from accidentally doing:
Transfer(amount, to, from) // wrong order, but compiles fine
// But if you do:
type AccountID int
type Bitcoin int
func Transfer(from AccountID, to AccountID, amount Bitcoin) {}
// The compiler catches your mistake