ProgrammingBackend Developer

What are the embedded Stringer and Error methods in Go, for what purposes are they used, and how to correctly implement them for your structures?

Pass interviews with Hintsage AI assistant

Answer.

In Go, the fmt.Stringer and error interfaces are used to control how a structure's value is converted to a string and how it implements an error, respectively. These interfaces provide universal ways of logging, outputting, and handling errors, making the code more flexible and understandable.

Background:

From the early versions of Go, the Stringer interface has been key to nicely controlled output of structures. The error interface has proven fundamental for error handling at all levels of code.

Problem:

Often programmers get uninformative output or unexpected error messages because they have not implemented these methods or have done so non-standardly. Additionally, incorrect implementation can lead to recursive output, panics, and unreadable errors.

Solution:

  • Implement the String() string method for structures if you need to manage their representation in fmt.Print*
  • Implement Error() string for custom error types

Example code:

package main import "fmt" type User struct { Name string ID int } func (u User) String() string { return fmt.Sprintf("User<%d:%s>", u.ID, u.Name) } type MyError struct { Msg string } func (e MyError) Error() string { return "MyError: " + e.Msg } func main() { u := User{Name: "Bob", ID: 10} fmt.Println(u) // calls String() err := MyError{Msg:"fail"} fmt.Println(err) // calls Error() }

Key features:

  • The String() and Error() methods are called atomically during output or logging
  • Incorrect implementation of String() can lead to infinite recursion if fmt.Sprintf is called inside it
  • Standardizing errors through Error() simplifies handling and tracing

Tricky Questions.

Is it mandatory to implement String() or Error() as value methods, or can pointers be used?

Both options are allowed, but implementation on pointer-receiver and value-receiver affects which types of objects the method will work on. Generally, pointer-receiver is used for mutable structures.

func (u *User) String() string {...}

Can fmt.Sprintf be used inside String() or Error()?

Yes, but you must carefully watch for infinite recursion (e.g., outputting a structure of the same type inside String()). It is recommended to avoid using fmt.Print inside String() if String() will be called again internally.

func (u User) String() string { return fmt.Sprintf("%v", u.Name) } // safe

What happens if the Error() method returns an empty string?

Errors with an empty string are treated as valid error values, but logging loses its meaning. The error interface does not define behavior for an empty string, but it is common practice to always provide an informative message.

Common Mistakes and Anti-patterns

  • Recursive call of fmt.Sprintf in String()
  • Implicit loss of information in Error()
  • Method names are string-like but not exported (syntax error)

Real-life Example

Negative Case

A developer outputs a structure using %+v without implementing String(), resulting in junk dumps of fields in the logs.

Pros:

  • Quick, no cost for nice output

Cons:

  • Logs are unreadable, hard to maintain, looks unsightly on user output

Positive Case

The team lead makes the team implement String() and Error() for all public structures. As a result, the business logic handles errors centrally, and the admin panel and debug logs become readable.

Pros:

  • Transparent error tracing
  • Clear, controlled output of structures

Cons:

  • Must be manually maintained when the structure changes