Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
109: Custom Stringer Interface
Imagine you're at a massive tech conference. You're surrounded by hundreds of people. Now, if I asked you to "describe" the person standing next to you, you wouldn't start by listing their blood type, their exact height in millimeters, and the date of their last dental cleaning. That would be a nightmare to listen to, and it's far too much raw data.
Instead, you'd probably look at their nametag. The nametag is a curated, simplified representation of that complex human being—usually just their name and their company. The person is the "object," but the nametag is the "Stringer" interface. It tells the world: "Whenever you need a text representation of me, just use this specific format."
In Go, when you print a struct using fmt.Println, the language does its best to show you the raw data inside. It's like reading a medical chart instead of a nametag. If you have a User struct, you get something like {John Doe john@example.com 12345}. It's functional, but it's ugly and not helpful for logs or user-facing output.
Telling Go how to describe your types
To fix this, we use the fmt.Stringer interface. It is one of the most common interfaces in the Go standard library, and it's incredibly simple. It only requires one method: String() string.
Let's say we're building a system to monitor servers. Without a custom Stringer, our server logs look like a mess of brackets. Here is how I'd handle that:
package main
import (
"fmt"
)
type Server struct {
Hostname string
IP string
Status int // 0: Down, 1: Up
}
// This is where the magic happens. By adding this method,
// Server now implements the fmt.Stringer interface.
func (s Server) String() string {
statusText := "Down"
if s.Status == 1 {
statusText = "Up"
}
return fmt.Sprintf("Server [%s] (%s) is currently %s", s.Hostname, s.IP, statusText)
}
func main() {
webServer := Server{
Hostname: "prod-web-01",
IP: "10.0.0.1",
Status: 1,
}
// I'm not calling .String() explicitly.
// fmt.Println detects that Server implements Stringer and calls it for me.
fmt.Println(webServer)
}
Now, instead of {prod-web-01 10.0.0.1 1}, you get Server [prod-web-01] (10.0.0.1) is currently Up. It's clean, it's readable, and it's decoupled from the logic of your main program.
The infinite loop trap
I've seen a lot of developers trip over this one, so listen closely: never call a printing function that triggers the Stringer method inside the String() method itself.
If you do something like return fmt.Sprintf("%v", s) inside the String() method of s, you've created an infinite recursive loop. fmt.Sprintf sees that your type has a String() method and calls it... which calls fmt.Sprintf... which calls String()... until your program crashes with a stack overflow. If you need to print the underlying fields of the struct within the method, be explicit about the fields or cast the object to a different type.
When to actually use this
I don't think every single struct needs a String() method. If you're just passing data around internally, the default Go formatting is fine. But use the Stringer interface when:
- You're creating a custom type (like an enum) where the integer value means nothing to a human.
- You're writing a library and want to provide a clean way for users to log your objects.
- You find yourself manually formatting the same struct into a string in five different places in your code.
📋 Practical Task
Formatting a Currency Display
You are building a financial application. Currently, the Money struct prints in a raw format that is confusing to users. Your task is to implement the fmt.Stringer interface to make the output professional.
Requirements:
- Create a struct named
Moneywith two fields:Amount(float64) andCurrency(string). - Implement the
String()method so that when aMoneyobject is printed, it displays as:"[Currency] [Amount]"(e.g.,"USD 100.50"). - Ensure the amount is formatted to exactly two decimal places.
- In your
mainfunction, create an instance ofMoneyand print it usingfmt.Printlnto verify that your custom stringer is being called automatically.
There are no comments for now.