Implementing 'Interfaces and struct embedding' in library package

I have a library package that calls out to an web service, to authenticate credentials. It’s basically this:

package myauthsvc

// Client object
type Client struct {
	AccessToken string
	Secret      string
}

func (c *Client) Authenticate(userID string, secret string) (bool, error) {
    // do some http call, return the result
    return false, nil
}

It’s working fine, but obviously hard to mock and not idiomatic. I’m reasonably clear about interfaces, when used between packages. But I’m getting confused about struct embedding specifically when it comes to a library package.

If I want to improve my implementation, is this correct?

package myauthsvc

type ClientInterface interface {
	Authenticate(userID string, secret string) (bool, error)
}

type Client struct {
	AccessToken string
	Secret      string
	ClientInterface
}

func (c *Client) Authenticate(userID string, secret string) (bool, error) {
	// do some stuff

	return false, nil
}

func NewClient(token string, secret string, ci ClientInterface) *Client {
	return &Client{token, secret, ci}
}

Firstly, The NewClient method is defined in the package, but outside the type/interface.

Secondly, the Authenticate method has a receiving type of Client. And I think this is where my lack of understanding is. Is there something implicit that means Client ‘conforms’ to ClientInterface, and selector shorthand is causing Client.Authenticate == Client.ClientInterface.Authenticate?

Lastly, if my library package is self-contained but I want to be able to make it easy for other developers to mock the methods, how does the implementation look?

package main

import (
    "github.com/thisdougb/myauthsvc" // this repo is just as an example, doesn't really exist
)
	
func test() {
	c := myauthsvc.NewClient("userid", "secret", ci) // <- where does ci come from?
}

How do I create ‘ci’ when creating a new client?

I have read through solid-go-design
And also type-embedding

thanks for help.

For anyone else on the same path, I think I’ve figured out where I was going wrong after trying out a good few things.

My motivation is to make my library package easy to mock, for other people. This isn’t what ‘embedding interfaces in structs’ does, it turns out. That’s my first deduction.

To make a library package easy to mock/test for the people using it, you include an interface type. This ClientInterface (package interface?) can then be used in method signatures as a parameter type. This, specifically, is what I was getting wrong.

package myauthsvc

type ClientInterface interface {
	Authenticate(userID string, secret string) bool
}

type Client struct {
	AccessToken string
	Secret      string
}

func (c Client) Authenticate(userID string, secret string) (bool, error) {
	// do some stuff with c.AccessToken and c.Secret
	return false
}

So, when someone wants to use myauthsvc in their own code we use normal dependency injection. This is possible because myauthsvc implements an interface. In standard ‘interface’ usage, ci accepts anything that implements the methods of ClientInterface.

// handler.go
import (
    "github.com/thisdougb/myauthsvc" // this repo is just as an example, doesn't really exist
)

func auth(userID string, secret string, ci myauthsvc.ClientInterface) bool {
	return ci.Authenticate(userID, secret)
}

What this then allows us to do, is to mock myauthsvc for our tests. I setup a test like this. A mock struct that also conforms to ClientInterface.

// handler_test.go
type MockAuthSvc struct{}

func (m MockAuthSvc) Authenticate(userID string, secret string) bool {
	return true
}

func TestAuth(t *testing.T) {

    var mockC MockAuthSvc
	assert.Equal(t, true, auth("userID", "secret", mockC)
}

So I can import myauthsvc as a third party library, and mock it easily to be able to run my own package tests.

I hope you can find those articles related to your case very useful:

https://github.com/golang/go/wiki/CodeReviewComments#interfaces – official Golang WIKI

https://www.ardanlabs.com/blog/2016/10/avoid-interface-pollution.html

https://rakyll.org/interface-pollution/ – comparison with C++/Java from Joanna

https://blog.chewxy.com/2018/03/18/golang-interfaces/ — Java comparison


Golang and interfaces misuse

Francesc Capmpy’s presentation “understanding the interface”:


yeah, those were good to read. thanks.

I guess the clearest thing being said is. “define interfaces at the point of use.” Which is the reverse of what I had been trying to do.

so my external library package becomes:

// external library package
package myauthsvc

type Client struct {
	AccessToken string
	Secret      string
}

func (c Client) Authenticate(userID string, secret string) (bool, error) {
	// do some stuff with c.AccessToken and c.Secret
	return false
}

my main app now looks like:

// handler.go
package handler

// doesn't import myauthsvc

type Authenticator interface { // defined at point of use
    Authenticate(userID string, secret string) (bool, error)
}

func auth(userID string, secret string, auth Authenticator) bool {
	return auth.Authenticate(userID, secret)
}

and I’m using the handler like this:

// main.go
package main

import (
    "github.com/thisdougb/myapp/handler"
    "github.com/thisdougb/myauthsvc" // this repo is just as an example, doesn't really exist
)

func test() {
    var authClient myauthsvc.Client
    result := handler.auth("userID", "secret", authClient)
}

I am thinking of this as, in the handler sub package I define the minimum behaviour required and call that an interface. I am then able to create concrete types which satisfy that interface, and pass them in. The concrete types can be from my external library package (myauthsvc), or a mocked object created in handler_test.go.

And what is now obvious, is that I can extend myauthsvc without breaking the handler implementation. Previously I’d have broken it, if my interface was larger and inside my external package.

And testing is really easy too, which was my original motivation.

thanks for all the links, very useful. :+1:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.