Rethinking DTOs in Go: Using Structs and Interfaces for Clean Data Transfer
This article explains how Go's native structs and interfaces can replace traditional Data Transfer Objects, offering a more concise, performant, and maintainable way to move data across application layers while highlighting best‑practice patterns and potential pitfalls.
Introduction
In many languages, Data Transfer Objects (DTOs) are used to carry data between layers, but Go’s type system and interface design allow more flexible solutions. This article explores common Go patterns that achieve DTO‑like functionality without extra boilerplate.
Go Types and Interfaces
Go emphasizes simplicity and efficiency. Its two core constructs for data handling are:
Structs : Define and encapsulate data, often used directly to pass information between layers, eliminating the need for separate DTO types.
Interfaces : Declare method sets; any type implementing those methods satisfies the interface, providing powerful abstraction and polymorphism.
Strategies that Replace DTOs
Instead of defining explicit DTOs, Go projects typically adopt one of the following approaches:
Direct struct usage : Define a struct and pass it through the layers, reducing redundant code.
Interface isolation : Create interfaces that describe required operations, then pass structs that implement those interfaces, improving modularity and testability.
Type embedding and composition : Embed one struct within another to extend functionality, useful for handling complex data structures.
Direct Struct Example
Consider an e‑commerce platform that needs to transfer order data from the service layer to the data‑access layer. A simple Order struct can hold fields such as UserID, ItemList, and PaymentDetails:
type Order struct {
UserID string
ItemList []Item
PaymentDetails Payment
}
func processOrder(o Order) {
// order processing logic
}This struct can travel from the API layer to business logic and finally to the database without additional conversion, keeping the code clean and efficient.
Advantages and Considerations
Advantages :
Performance : No extra memory allocation or object wrapping.
Simplicity : Go’s minimalist design avoids over‑engineering.
Considerations :
Data‑structure changes : Modifying a struct may impact many parts of the application.
Interface misuse : Overusing interfaces can hurt readability and performance.
Interface Isolation Example
To achieve stronger decoupling, define an OrderData interface that exposes only the needed getters:
// OrderData defines the contract for order information
type OrderData interface {
GetID() string
GetCustomerID() string
GetOrderDetails() []OrderDetail
}
type OrderDetail struct {
ProductID string
Quantity int
Price float64
}
type ConcreteOrder struct {
ID string
CustomerID string
Details []OrderDetail
}
func (co *ConcreteOrder) GetID() string { return co.ID }
func (co *ConcreteOrder) GetCustomerID() string { return co.CustomerID }
func (co *ConcreteOrder) GetOrderDetails() []OrderDetail { return co.Details }Next, modify the OrderProcessor interface to accept OrderData instead of a concrete Order:
// OrderProcessor processes orders via the abstract interface
type OrderProcessor interface {
ProcessOrder(order OrderData) error
}
type APIService struct{}
func (api *APIService) ProcessOrder(order OrderData) error {
fmt.Println("Processing order ID:", order.GetID())
return nil
}Using this pattern, any implementation of OrderProcessor works with any struct that satisfies OrderData, enabling reuse across services or test environments without changing the processor code.
func main() {
apiService := APIService{}
order := ConcreteOrder{
ID: "12345",
CustomerID: "abcde",
Details: []OrderDetail{{ProductID: "xyz", Quantity: 2, Price: 29.99}},
}
// Pass the order via the interface
apiService.ProcessOrder(&order)
}Conclusion
Go’s flexible type system allows developers to handle cross‑layer data transfer efficiently without the overhead of traditional DTO patterns. By leveraging structs for simple cases and interfaces for abstraction, code remains concise, performant, and easier to maintain.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ops Development & AI Practice
DevSecOps engineer sharing experiences and insights on AI, Web3, and Claude code development. Aims to help solve technical challenges, improve development efficiency, and grow through community interaction. Feel free to comment and discuss.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
