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 {}
Listing 8.1: Defining an 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)
}
Listing 8.2: Keep interfaces small, no more than two or three methods.

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
}
Listing 8.3: Interfaces are limited to methods only.

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 {
Listing 8.4: The Insert method.

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)
	}

	// retreive 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.453s

--------------------------------------------------------------------------------
Go Version: go1.19
Listing 8.5: Passing a function type to the Insert method.

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
}

Listing 8.6: The Model interface.

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 {
Listing 8.7: Changing the Insert method to accept a Model interface.

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)
	}

	// retreive 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 type Model in argument to s.Insert:
	func() does not implement Model (missing ID method)
./store_test.go:27:11: impossible type assertion: act.(func())
	func() does not implement Model (missing ID method)

--------------------------------------------------------------------------------
Go Version: go1.19
Listing 8.8: Rejecting types that implement Model.

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() int

Listing 8.9: The User type implements the Model interface.

When 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)
	}

	// retreive 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.413s

--------------------------------------------------------------------------------
Go Version: go1.19
Listing 8.10: Using the User type.

Subscribe to our newsletter