On Accepting Interfaces and Structs

327 views
Skip to first unread message

Kaveh Shahbazian

unread,
Apr 21, 2018, 7:51:55 AM4/21/18
to golang-nuts
Regarding "Accept interfaces, return concrete types", how can it be applied for structs that represent a payload/value?

For example in package first, logger is defined as:

type logger interface {
   
Debugf(template string, args ...interface{})
   
Errorf(template string, args ...interface{})
   
Infof(template string, args ...interface{})
}

And package first only accepts a logger that implements the logger interface.

Now lets assume there is a need for passing a struct too, like some config or state.

This causes importing the original package that, that config or state struct resides in; while package first is happily accepting other things from that package using interfaces.

For example in package second there is some tool that is represented using this interface in package first:

type cloner interface {
   
Clone() (*second.State, error)
}


As it can be seen, now package first has to explicitly import package second, because of the type *second.State.

Currently I break things by renaming the second package to something meaningless when importing like:

type cloner interface {
   
Clone() (*p2nd.State, error)
}

But this is not really a work around and package second leaks into the scope of package first anyway.

Is there a way to actually achieve this?

Axel Wagner

unread,
Apr 21, 2018, 8:06:40 AM4/21/18
to dc0d, golang-nuts
On Sat, Apr 21, 2018 at 1:52 PM Kaveh Shahbazian <kaveh.sh...@gmail.com> wrote:
Is there a way to actually achieve this?

Either change `second.cloner` to return an interface, or (IMO better) just import `second`. I don't understand why you'd want to avoid that.

Kaveh Shahbazian

unread,
Apr 21, 2018, 12:53:14 PM4/21/18
to golang-nuts
You might be right. Probably I am fixating on something that I do not understand well and just have a not positive feeling about it. But two things:

1 - Other packages (will) have implementations that satisfies first.cloner so there might be:

type cloner interface {
    Clone() (*third.State, error)
}

Should I put the State struct in it's own package? (Seems to be a logical solution.)

2 - Being forced to import the dependency explicitly, while I expect just to be able to accept it as an interface, in a NewX constructor, is nullifying the whole fantastic game of interfaces. State is a POGO (as in POJO or POCO - plain old Go object, just an analogy).

Louki Sumirniy

unread,
Apr 21, 2018, 8:48:00 PM4/21/18
to golang-nuts
I only just finally wrapped my head around this stuff and forgive me if I have missed the point of the question but this is what my code has:

type AbstractType alias/struct {}

type abstractthing interface {
  DoSomething(interface{})
}

func (a *AbstractType) DoSomething(b AbstractType) {

}

and then in my implementer:

import ( 
  "previousclass/path"
)

type ConcreteType alias/struct {}

func (c *ConcreteType) DoSomething(t ConcreteType) {

}

You have to create a dummy function in the abstract type's source file in order to use it in the superclass, as it effectively is, in order to use its generalised functionality, but your app imports the second one, which sucks up everything from the first and the function bound to the concrete type overrides the abstract functions in the superclass, allowing you to generalise part of the superclass and enable you to write a set of functions with part of the implementation (for example, a tree store) while letting you change the data type to something else. It's composition, as opposed to inheritance.

Kaveh Shahbazian

unread,
Apr 22, 2018, 7:59:06 AM4/22/18
to golang-nuts
The State type is just a POGO with no methods. The part that changes is the implementation of Clone() (State, error) method.

jake...@gmail.com

unread,
Apr 23, 2018, 12:08:38 PM4/23/18
to golang-nuts
My gut feeling is that there is an elegant way to achieve what you want. Unfortunately, I am having trouble following the code you are describing, and more importantly,  what you are actually trying to accomplish. Perhaps it is my failing.

If you would post a more complete example, showing the relevant code in all the related packages, I suspect you would get constructive suggestions.

- Jake

Bryan Mills

unread,
Apr 23, 2018, 4:02:33 PM4/23/18
to golang-nuts
I agree with Jake: a more complete example would be helpful. In my experience, this sort of issue is often a symptom of awkward package boundaries. If you can find a more natural package boundary, you may be able to sidestep the problem.

(There are certainly some cases where the lack of covariance makes interfaces difficult to construct, but those cases tend to be rare.)

Kaveh Shahbazian

unread,
Apr 24, 2018, 1:49:45 AM4/24/18
to golang-nuts
@Jake @Bryan Thanks!

Current solution I use:

A package that holds the shared data type State:

package state

type
State struct {
   
Latitude  float64
   
Longitude float64
   
Mark      string
   
Name      string
   
// ...
}

Three packages with different implementations and algorithms:

package concrete1

import (
   
"gitlab.com/dc0d/gist/2018/Q1/draft/cmd/draft/state"
)

type
Concrete1 struct{}

func
(c *Concrete1) Clone() (*state.State, error) { panic("N/A") }

And:

package concrete2

import (
   
"gitlab.com/dc0d/gist/2018/Q1/draft/cmd/draft/state"
)

type
Concrete2 struct{}

func
(c *Concrete2) Clone() (*state.State, error) { panic("N/A") }

And:

package concrete3

import (
   
"gitlab.com/dc0d/gist/2018/Q1/draft/cmd/draft/state"
)

type
Concrete3 struct{}

func
(c *Concrete3) Clone() (*state.State, error) { panic("N/A") }

And the consumer package, which will be given a concrete implementation based on some logic (this is where "Accepting interfaces" is happening):

package consumer

import (
   
"gitlab.com/dc0d/gist/2018/Q1/draft/cmd/draft/state"
)

func
Use(cln cloner) {}

type cloner
interface {
   
Clone() (*state.State, error)
}

The part in question is the *state.State data type. This is the best I could come up with.

But I do not like:

  1. Having a package just to hold a single type,
  2. The consumer package is accepting a concrete type and depends on it - seems this one can not be avoided since there is nothing like structural typing for structs in Go.

There might be a better way to do this.

Louki Sumirniy

unread,
Apr 24, 2018, 2:06:29 AM4/24/18
to golang-nuts
The name could use some work, stutter is a no-no in Go. What kind of state does it hold? User profiles? MMO game world database? Is your scope too broad? I see that it looks like a geography database, so it would make more sense to call it 'geo' or something like this. Also, for such a database system there is waypoints, paths and regions as subtypes that I can think of off the top of my head. Or maybe 'location' is a better name for it. Will there be a history ledger also?

I hope that helps you... it's just a namespace question really. The important thing about how to define it has to do with what will be done with it not what it is, as a matter of ontology. Object oriented design methodology tends to look at things like everything can be abstracted and in the real world, categories are containers, like 'car' is the general, 'engine', 'transmission', 'wheels', and inside engine is 'carb/injector' 'pistons' 'cams' 'ignition' inside ignition is ignition timing, throttle etc etc. If you follow a model of containers you will get clean boundaries between things, and the path from user to data will be much more clear.

Kaveh Shahbazian

unread,
Apr 24, 2018, 2:58:46 AM4/24/18
to golang-nuts
Thanks @Louki,

The names are absolutely made up and are just for the purpose of demonstration.

The problem is the package state, one whole package to just hold a type.

Louki Sumirniy

unread,
Apr 24, 2018, 3:19:56 AM4/24/18
to golang-nuts
I looked at the OP again and I have created a struct embedding with interface type for a similar purpose as you have in mind. 

I'm not sure how to condense it exactly, but I'll just explain the salient points:

  1. The data has to be referred to via an interface{} which you load with a struct, and inside the struct is your storage structure, which can be just a slice array, and in my case it was necessary to embed because slices don't talk with interface{} in go
  2. The functions that are specific to the data type have to be overridable, so that those that are general can call these functions without knowing or caring about what the actual data type is. For this I added the function collection to the primary struct defining the superclass, if I can call it that, so that you can then use abstract calls that pass through the interface to the implementation (which can be in a separate source file if the names are capitalised to be exported)
  3. Type specifications for all of the type-specific functions need to be made, and these form the list of functions inside the superclass type struct, and then you create an initialiser that loads in the specific implementation functions
One little tip I can give with the initialiser is that it can be easier to use closures to specify the type specific function set. Someone pointed out that my code was missing the search/insert/delete functions and I actually didn't realise I'd removed them as I had actually written them previously... but part of the reason I probably did that was because I didn't yet know (until pretty much yesterday or the day before) how to bind and pass interface variables around... so I will need to put those functions back in now that I know how to abstract the data accessors and pass through values from the caller without needing to know their types.

Ideally you narrow the amount of parts that deal with the abstractions as much as possible, without sacrificing the readability of your code, though that's a matter of personal preference (I like code that reads like bad english - not english, but still understandable). In my case, the search/insert/delete functions could be part of the function set bound to the type, but they can stay in the higher level of the code if you don't address their types directly but use the accessor functions set to do this for you, then inside these functions all the types are known, but the operations can be tailored to the concrete types.

So in other words, there is a number of ways to do various different parts of it and you'll need to read up on them to make your own decision how you want to implement it. I won't say that my code is exemplary but it will at least give you some clues as to at least the options I have decided to use for this.

Louki Sumirniy

unread,
Apr 24, 2018, 3:27:24 AM4/24/18
to golang-nuts
In the process of answering your questions I realised there was a number of changes I needed to make to my code, here is the state of it at the commit I just made with this in mind: (this is a permalink and shows how it is at this exact stage) https://github.com/calibrae-project/bast/tree/418af6fc24480cbbc2d7b72bab794f0375439f9c

jake...@gmail.com

unread,
Apr 26, 2018, 8:15:33 PM4/26/18
to golang-nuts
The example gives me a better understanding of what you are doing, though still not much understanding as to why you are doing it this way. Usually, if I feel that code has a "smell" in Go, there is a way to rethink the basic design resulting more "natural" code. But without a full understanding of the real situation, it is impossible to say if that applies here. But lets assume for now that you must have multiple packages that all contain functions that return a common struct.

 
But I do not like:

  1. Having a package just to hold a single type,
Personally, I don't see anything inherently wrong with this. If you must multiple packages that all need the type, then it should be in a package separate from those. In many cases there would be functions that could also go in package `state`, but if not, then that's ok too.

2. The consumer package is accepting a concrete type and depends on it - seems this one can not be avoided since there is nothing like structural typing for structs in Go.

Your example does feel a bit awkward, but, again, I don't have enough information on what is really being achieved to suggest a completely different model.  

But I will take a shot in the dark. Think interfaces? Perhaps the state.State type returned by Clone() could be an interface? This does not solve the problem of having to define the interface in a separate package, and I would only use an interface if there was a compelling reason to do so. 

Taking a further leap,why "Clone()" at all? What do you do with state.State in package consumer? Does it have to be a struct for some reason? If you could define a set of functionality that a consumer needs from state.State, you could then have Concrete1, Concrete2, and Concrete3 all implement that functionality. Then consumer can just use them directly as interfaces ... or perhaps that is using them indirectly. Anyway, I hope that is somewhat comprehensible. 

Good Luck
 - Jake

Kaveh Shahbazian

unread,
Apr 27, 2018, 11:49:51 AM4/27/18
to golang-nuts
Assume the state is the config struct and each implementation is reading it from s different file type (yaml, toml, hlc, json, etc).
Reply all
Reply to author
Forward
0 new messages