I want to describe a scenario where there currently isn’t a good solution in Go (at least that I am aware of). Let’s say you have something like the template.Template type with methods like Template.Funcs:
type Template struct {
*parse.Tree
// contains filtered or unexported fields
}
func (t *Template) Funcs(funcMap FuncMap) *Template
If we wanted to use an interface for this type, it is impossible to express in Go now. At first you would think that you could use an interface like this:
type Funcser interface {
Funcs(FuncMap) Funcser
}
But if you try it out in Go it won’t work. While our template.Template
type appears to match this definition, template.Funcs
method returns the *template.Template
type, NOT the Funcser
type, and that means it wont implement this interface.
Truthfully, I’m not certain that every implementation of generics would solve this issue, nor do I feel that generics are required to solve the issue, but if generics are added to Go I’d love to see an implementation that somehow makes this situation a little better.
Know of a better solution?
If you happen to know of a better solution to this problem let me know. I may just be ignorant to better solutions and if so I’d love to hear about them 🙂
It was pointed out (and I agreed) that this post is a little short on details. I’m going to try to expand a bit here in order to give a better example of why this matters and how it can affect a real application.
According to the Experience Reports wiki:
The best experience reports tell: (1) what you wanted to do, (2) what you actually did, and (3) why that wasn’t great, illustrating those by real concrete examples, ideally from production use.
I’ll start with these three points, as I think they are enough to really illustrate the experience.
(1) what you wanted to do
Using GORM is where this comes up most frequently, but it has happened with other packages in the past. In GORM you typically create queries by chaining something like this:
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
My issue is that the second I introduce this code into my application, I can’t easily test it because I can’t replace the *gorm.DB
type with an interface. I now need either a live database connection for my tests, or I need to write a fairly large wrapper around GORM. That is, I can’t write code like this:
type WhereOrFinder interface {
func Where(...) WhereOrFinder
func Or(...) WhereOrFinder
func Find(...) *gorm.DB
}
func admins(db WhereOrFinder) []User {
var users []Users
err := db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users).Error
return users, err
}
And then test it with a mock.
Now before I move on, I’m guessing that most people are going to stop here and say, “Woah, you returned *gorm.DB on that Find
method! Why not return it for all of the methods?” This doens’t work because then the chained methods would no longer be using the mock. I can create a mock like this:
type mockDB struct {}
func (m mockDB) Where(...) mockDB { return m }
func (m mockDB) Or(...) mockDB { return m }
func (m mockDB) Find(...) *gorm.DB { return &gorm.DB{Error: someError} }
And it will work because my code doesn’t leave the mock until the final result is returned, but if I were to instead create a mock like this:
type WhereOrFinder interface {
func Where(...) *gorm.DB
func Or(...) *gorm.DB
func Find(...) *gorm.DB
}
type mockDB struct {}
func (m mockDB) Where(...) *gorm.DB {
// If I return a gorm.DB here the rest of my code will interact
// with a REAL gorm db, not my mock!
}
func (m mockDB) Or(...) *gorm.DB { return ... }
func (m mockDB) Find(...) *gorm.DB { return &gorm.DB{Error: someError} }
You could make the argument that occasionally I will want to hit a live DB in my tests and I agree, but I don’t want to do that for literally every single test that uses gorm.DB
. It slows things down, and it means I need to do some sort of DB “reset” (or rollback) after each test to make sure I start with a clean slate. In an ideal world I would instead be testing with a mock.
(2) what you actually did
One of two things:
I tend to end up going with Option 1 most often because it is clear what is going on and that we use GORM, whereas option 2 requires me to figure out all the methods we might use w/ GORM, make sure other devs know to add new ones they need if they aren’t in the mock, and so on.
(3) why that wasn’t great, illustrating those by real concrete examples, ideally from production use.
Interfaces are, in my opinion, great in Go because they are easy to use and because someone creating a type doesn’t have to go out of their way to define all the interfaces it implements. That is, I don’t have to write something like:
type Dog implements <Sortable, Stringer, ...>
Duck typing and the simplicity it brings to Go’s interfaces are (again imo) what allows the “accept interfaces, return structs” mantra to work.
Having to create a wrapper or simply not use an interface isn’t great because it is directly contradictory to this point. When we have a type that returns itself in a method, package developers must explicitly define the interface themselves, or users of the type will need to create an often large wrapper to enable that behavior. You can see this in the example I linked above for a GORM wrapper - the code there is required to overwrite every single method we might use in the original gorm.DB
type and update them all to return a specific interface instead. It works, but it just never felt ideal.
It is probably also worth noting that this is a big part of the reason why I tend to push back when someone wants to use method chaining in Go instead of another approach like functional options.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.