I am building an Todo CLI tool recently and used Go’s powerful interface to achieve better formatting.
Previously my cli looks like:
$ ./todo -list
Another ToDo item
Improve usage
Improve output
And the code looks like:
l := &todo.List{}
switch {
case *list:
for _, t := range *l {
if !t.Done {
fmt.Println(t.Task)
}
}
But I don’t just want to print out the items I put inside the list, I want to items printed out with number and a mark “X” to indicate if the item was completed or not.
Here Comes the Interface:
There are several strategies to enhance the formatting of output. For example, if you don’t have control over the API code, your only option might be to format the output within the implementation of the command-line tool. However, since you own the API, you can take advantage of Go’s robust Interfaces feature to directly implement the list output formatting in the todo.List type. This ensures a uniform output format for anyone using your API.
In Go, an interface establishes a contract. Unlike in some other languages, Go interfaces only define behavior, not state. This means an interface specifies what a type should do, not what data it should contain.
To fulfill an interface, a type only needs to implement all the methods defined in the interface with the same signature. Furthermore, explicit declaration is not required to satisfy an interface. Types implicitly implement an interface by defining all its methods.
This is a powerful concept that significantly influences how interfaces are utilized. By implicitly satisfying an interface, a type can be used wherever that interface is expected, promoting code decoupling and reuse.
Now we’ll implement the Stringer interface on the todo.List type. The Stringer interface is defined in the fmt package as follows:
type Stringer interface {
String() string
}
Any types that implement the method String(), which returns a string, satisfy the Stringer interface. By satisfying this interface, you can provide the type to any formatting function that expects a string.
To implement the Stringer interface on the todo.List type:
In Go, the Stringer interface is defined in the fmt package as follows:
type Stringer interface {
String() string
}
Any type that has a method with this signature (String() string) is said to implement the Stringer interface. This method is used to provide a “stringified” representation of the type.
List is a type that represents a slice of item. It implements the Stringer interface by defining a String() method. This method iterates over the items in the list and builds a string representation of the list.
When we do fmt.Print(l), where l is of type *List, the fmt package checks if l implements the Stringer interface. If it does (which is the case here), fmt calls the String() method to get a string representation of l, and then prints that string.
So, l := &todo.List{} creates a pointer to a new, empty List. When we pass l to fmt.Print, it calls l.String() to get a string representation of the list, which is then printed. This is how the Stringer interface is used in this case.
The Stringer interface is a common way in Go to specify how types should be converted to strings, especially when you want to control the string format of complex types.
What It Looks Like Afterwards:
The relevant code should be changed accordingly:
Instead of looping through the list again, we could just print out the list, because below String() method was implemented by List already:
func (l *List) String() string {
var str string
for k, t := range *l {
prefix := " "
if t.Done {
prefix = "X"
}
str += fmt.Sprintf("%s%d: %s\n", prefix, k+1, t.Task)
}
return str
}
And the output in the terminal became:
That’s exactly what I want to achieve so far.
Another Example:
let’s consider a simple example where we have a Person struct and we want to control how it’s printed. We can do this by implementing the Stringer interface for the Person type.
we define a Person struct with Name and Age fields. We then implement the Stringer interface by defining a String() method on the Person type. This method returns a string that represents a Person instance in the format we want.
When we print a Person instance using fmt.Println, it automatically calls our String() method to get a string representation of the Person, and then prints that string.
Notice that now you can call the fmt.Print() function, which requires no format specifier, as the format comes from the Stringer interface implemented by the variable l of type todo.List.
Source Code:
GitHub - LordMoMA/TODO-CLI: A smart TODO CLI to improve your work effeciency
Source Code evolves and might not be the same as the above code shows, please check the new features in the source code below: