I'm familiar with the fact that, in Go, interfaces define functionality, rather than data. You put a set of methods into an interface, but you are unable to specify any fields that would be required on anything that implements that interface.
For example:
// Interface
type Giver interface {
Give() int64
}
// One implementation
type FiveGiver struct {}
func (fg *FiveGiver) Give() int64 {
return 5
}
// Another implementation
type VarGiver struct {
number int64
}
func (vg *VarGiver) Give() int64 {
return vg.number
}
Now we can use the interface and its implementations:
// A function that uses the interface
func GetSomething(aGiver Giver) {
fmt.Println("The Giver gives: ", aGiver.Give())
}
// Bring it all together
func main() {
fg := &FiveGiver{}
vg := &VarGiver{3}
GetSomething(fg)
GetSomething(vg)
}
/*
Resulting output:
5
3
*/
Now, what you can't do is something like this:
type Person interface {
Name string
Age int64
}
type Bob struct implements Person { // Not Go syntax!
...
}
func PrintName(aPerson Person) {
fmt.Println("Person's name is: ", aPerson.Name)
}
func main() {
b := &Bob{"Bob", 23}
PrintName(b)
}
However, after playing around with interfaces and embedded structs, I've discovered a way to do this, after a fashion:
type PersonProvider interface {
GetPerson() *Person
}
type Person struct {
Name string
Age int64
}
func (p *Person) GetPerson() *Person {
return p
}
type Bob struct {
FavoriteNumber int64
Person
}
Because of the embedded struct, Bob has everything Person has. It also implements the PersonProvider interface, so we can pass Bob into functions that are designed to use that interface.
func DoBirthday(pp PersonProvider) {
pers := pp.GetPerson()
pers.Age += 1
}
func SayHi(pp PersonProvider) {
fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}
func main() {
b := &Bob{
5,
Person{"Bob", 23},
}
DoBirthday(b)
SayHi(b)
fmt.Printf("You're %v years old now!", b.Age)
}
Here is a Go Playground that demonstrates the above code.
Using this method, I can make an interface that defines data rather than behavior, and which can be implemented by any struct just by embedding that data. You can define functions that explicitly interact with that embedded data and are unaware of the nature of the outer struct. And everything is checked at compile time! (The only way you could mess up, that I can see, would be embedding the interface PersonProvider
in Bob
, rather than a concrete Person
. It would compile and fail at runtime.)
Now, here's my question: is this a neat trick, or should I be doing it differently?
It is definitely a neat trick. However, exposing pointers still makes direct access to data available, so it only buys you limited additional flexibility for future changes. Also, Go conventions do not require you to always put an abstraction in front of your data attributes.
Taking those things together, I would tend towards one extreme or the other for a given use case: either a) just make a public attribute (using embedding if applicable) and pass concrete types around or b) if exposing the data seems to complicate some implementation change you think is likely, expose it through methods. You're going to be weighing this on a per-attribute basis.
If you're on the fence, and the interface is only used within your project, maybe lean towards exposing a bare attribute: if it causes you trouble later, refactoring tools can help you find all the references to it to change to a getter/setter.
Hiding properties behind getters and setters gives you some extra flexibility to make backwards-compatible changes later. Say you someday want to change Person
to store not just a single "name" field but first/middle/last/prefix; if you have methods Name() string
and SetName(string)
, you can keep existing users of the Person
interface happy while adding new finer-grained methods. Or you might want to be able to mark a database-backed object as "dirty" when it has unsaved changes; you can do that when data updates all go through SetFoo()
methods. (You could do it other ways, too, like stashing the original data somewhere and comparing when a Save()
method is called.)
So: with getters/setters, you can change struct fields while maintaining a compatible API, and add logic around property get/sets since no one can just do p.Name = "bob"
without going through your code.
That flexibility is more relevant when the type is complicated (and the codebase is big). If you have a PersonCollection
, it might be internally backed by an sql.Rows
, a []*Person
, a []uint
of database IDs, or whatever. Using the right interface, you can save callers from caring which it is, the way io.Reader
makes network connections and files look alike.
One specific thing: interface
s in Go have the peculiar property that you can implement one without importing the package that defines it; that can help you avoid cyclic imports. If your interface returns a *Person
, instead of just strings or whatever, all PersonProviders
have to import the package where Person
is defined. That may be fine or even inevitable; it's just a consequence to know about.
But again, the Go community does not have a strong convention against exposing data members in your type's public API. It's left to your judgment whether it's reasonable to use public access to an attribute as part of your API in a given case, rather than discouraging any exposure because it could possibly complicate or prevent an implementation change later.
So, for example, the stdlib does things like let you initialize an http.Server
with your config and promises that a zero bytes.Buffer
is usable. It's fine to do your own stuff like that, and, indeed, I don't think you should abstract things away preemptively if the more concrete, data-exposing version seems likely to work. It's just about being aware of the tradeoffs.