Public Interface, Private Struct Technique

There is a technique call "public interface, private struct" being used in the wild. It was originally meant for abstracting very complex and composite functions into simple interface. When applying this technique wrongly, it can cost one to re-write the whole package. Let's learn more about this "public interface, private struct" technique.

About The Technique

This technique is commonly used to hide certain structure or complex functions composite as seen in io.MultiWriter standard package source codes. The technique is originally intended for abstracting structure functionalities.

To use it, one defines the "public" interfaces using the interface{} type and supplying a private private struct through some kind of function like New(). However, the private struct function must offer exported function complying to the "public" interface.

Here is an example where Calculator is a public interface and New() creates a *calculator private struct object where it has exported methods:

type Calculator interface{} {
  // Add is to add base and addition, returning the final sum value.
  //
  // In case you need a long documentations.
  Add(base int, addition int) int

  // Minus is to add base and addition, returning the final subtracted value.
  //
  // In case you need a long documentations.
  Minus(base int, addition int) int

  ...
}

// New is to create a Calculator object with initialized fields ready for use.
//
// It returns its pointer as a value,
func New() Calculator {
    return &calculator{
        ...
    }
}

type calculator struct {
  ...
}

func (c *calculator) Add(base int, addition int) int {
    ...
}


func (c *calculator) Minus(base int, addition int) int {
    ...
}

...

The Takes - Benefits

This techniques have some benefits when deployed in a package. Here are some of the key benefits from using it:

No Effect On Go Test

Go test, especially with code coverage mapping, is able to view the entire package and map out the coverage accordingly. Hence, you can continue to test your package with no issues.

A Screenshot of Code Coverage Map When I Tested This Technique

Keeping Highly Complex Subroutine Simple to Use

If you have a very complex under-the-hood functionalities that is though to offer up, this technique is definitely beneficial. However, let's be clear: you shouldn't be achieving this point at the first place so this benefit point is rarely achieved.

Allows Users to Easily Mock The Interface

Since the offered interface is an interface{} type, users can easily mock the interface by creating his/her own ones. This is especially useful in testing.

The Give - Bad Effects

While we see the benefits, now this technique has some bad sides.

Double Maintenance Efforts

If you're already defining a private structure with public function, why purposely encapsulate it with an interface{}? Each time when you add/remove/modify/delete any methods, you need to do double efforts of updating both the struct and the interface.

Horrible Go Document

On the side of Go Doc, it's a downside. Go Doc ONLY sees the interface as a single type definitions and squeezes everything in it and that's it. That means there is NO WAY to detail each of your interface functions in its own section like the conventional functions and methods are detailed out.

You are likely going to end up something like this as your documentation output if you apply the technique to your entire package:

type Calculator interface{} {
  // Add is to add base and addition, returning the final sum value.
  //
  // In case you need a long documentations.
  Add(base int, addition int) int

  // Minus is to add base and addition, returning the final subtracted value.
  //
  // In case you need a long documentations.
  Minus(base int, addition int) int

  ...
}

Comparing to the conventional:

type Calculator struct {
  // contain reexported fields
}
Calculator is a structure that operates Mathematics calculations.


func New() *Calculator
New is to create a Caculator object with initialized fields ready for use.

It returns its pointer as a value,


func (c *Calculator) Add(base int, addition int) int
Add is to add base and addition, returning the final sum value.

In case you need a long documentations.


func (c *Calculator) Minus(base int, addition int) int
Minus is to add base and addition, returning the final subtracted value.

In case you need a long documentations.


...

When to Use

Personally, I would AVOID deploying this technique for package level encapsulation. It makes no sense to do repeated work and there is no point absorbing a specific use case into the package which can be a waste for other use cases.

However:

  1. I would do it where the interface functions does not need any documentation explanatory, like a very specific interface.
  2. I would also do it to encapsulate a complex function that has large and complicated composite beneath it.

That's all I can think of its use case. For any others, I would stick back to Go coding conventions and the original intention of interface{}: abstracting all kinds of common data types.

There is no denial that this technique is a creative finding from many other Gophers. However, using interface{} as a package encapsulation is definitely a no go and one will waste time rewriting the entire package later in time. In fact, I would only recommend to use this technique only for complex objects like how many of the standard packages did.