Go Fundamentals - Sample
Table of Contents
Chapter 8.10: Defining Interfaces
You can create a new interface in the Go type system by using the type keyword, giving the new type a name, and then basing that new type on the interface type, Listing 8.1.
type MyInterface interface {}
Interfaces define behavior, therefore they are only a collection of methods. Interfaces can have zero, one, or many methods.
The larger the interface, the weaker the abstraction. – Rob Pike
It is considered to be non-idiomatic to have large interfaces. Keep the number of methods per interface as small as possible, Listing 8.2. Small interfaces allow for easier interface implementations, especially when testing. Small interfaces also help us keep our functions and methods small in scope making them more maintainable and testable.
type MyInterface interface {
Method1()
Method2() error
Method3() (string, error)
}
It is important to note that interfaces are a collection of methods, not fields, Listing 8.3. In Go only structs have fields, however, any type in the system can have methods. This is why interfaces are limited to methods only.
// valid
type Writer interface {
Write(p []byte) (int, error)
}
// invalid
type Emailer interface {
Email string
}
Defining a Model Interface
Consider, again, the Insert method for our data store, Listing 8.4. The method takes two arguments. The first argument is the ID of the model to be stored.
func (s *Store) Insert(id int, m any) error {
The second argument, in Listing 8.4, should be one of our data models. However, because we are using an empty interface, any type from int to nil may be passed in.
func Test_Store_Insert(t *testing.T) {
t.Parallel()
// create a store
s := &Store{
data: Data{},
}
exp := 1
// insert a non-valid type
err := s.Insert(exp, func() {})
if err != nil {
t.Fatal(err)
}
// retrieve the type
act, err := s.Find(exp)
if err != nil {
t.Fatal(err)
}
// assert the returned value is a func()
_, ok := act.(func())
if !ok {
t.Fatalf("unexpected type %T", act)
}
}$ go test -v
=== RUN Test_Store_Insert
=== PAUSE Test_Store_Insert
=== CONT Test_Store_Insert
--- PASS: Test_Store_Insert (0.00s)
PASS
ok demo 0.163s
--------------------------------------------------------------------------------
Go Version: go1.25.0
To prevent types, such as a function definition, Listing 8.5, that aren't an expected data model, we can define an interface to solve this problem. Since the Insert function needs an ID for insertion, we can use that as the basis for an interface.
type Model interface {
ID() int
}To implement the Model interface, Listing 8.6, a type must have a ID() int method. We can cleanup the Insert method's definition by accepting a single argument, the Model interface, Listing 8.7.
func (s *Store) Insert(m Model) error {
Now, the compiler and/or runtime will reject any type that, such as string, []byte, and func(), that doesn't have a ID() int method, Listing 8.8.
func Test_Store_Insert(t *testing.T) {
t.Parallel()
// create a store
s := &Store{}
exp := 1
// insert a non-valid type
err := s.Insert(func() {})
if err != nil {
t.Fatal(err)
}
// retrieve the type
act, err := s.Find(exp)
if err != nil {
t.Fatal(err)
}
// assert the returned value is a func()
_, ok := act.(func())
if !ok {
t.Fatalf("unexpected type %T", act)
}
}$ go test -v
FAIL demo [build failed]
# demo [demo.test]
./store_test.go:15:18: cannot use func() {} (value of type func()) as Model value in argument to s.Insert: func() does not implement Model (missing method ID)
./store_test.go:27:11: impossible type assertion: act.(func())
func() does not implement Model (missing method ID)
--------------------------------------------------------------------------------
Go Version: go1.25.0
Implementing the Interface
Finally, let's create a new type, User, that implements the Model interface, Listing 8.9.
type User struct {
UID int
}
func (u User) ID() intWhen we update the tests, Listing 8.10, to use the User type, our tests now pass.
func Test_Store_Insert(t *testing.T) {
t.Parallel()
// create a store
s := &Store{
data: Data{},
}
// create a user
exp := User{UID: 1}
// insert the user
err := s.Insert(exp)
if err != nil {
t.Fatal(err)
}
// retrieve the user
act, err := s.Find(exp.UID)
if err != nil {
t.Fatal(err)
}
// assert the returned value is a user
actu, ok := act.(User)
if !ok {
t.Fatalf("unexpected type %T", act)
}
// assert the returned user is the same as the inserted user
if exp.UID != actu.UID {
t.Fatalf("expected %v, got %v", exp, actu)
}
}$ go test -v
=== RUN Test_Store_Insert
=== PAUSE Test_Store_Insert
=== CONT Test_Store_Insert
--- PASS: Test_Store_Insert (0.00s)
PASS
ok demo 0.808s
--------------------------------------------------------------------------------
Go Version: go1.25.0