3

I understand the problem, as per the answer here, however, I could really use help or a more detailed code explanation of how it's overcome.

My situation is this: I used to have models and controllers separated, and in my models package I had a datastore.go file containing an interface of all the model functions:

package models

type DSDatabase interface {
    CreateUser(ctx context.Context, username string, password []byte) (*datastore.Key, error)
    // More model functions
}

type datastoreDB struct {
    client *datastore.Client
}

var (
    DB DSDatabase
    _  DSDatabase = &datastoreDB{}
)

func init() {
    // init datastore
}

This was all fine because the model functions were also located within the models package, so my functions in the controller package could freely call models.DB.CreateUser(ctx, "username", []byte("password")).

Now, I have decided to move all the above code to a datastore package, whereas the model for CreateUser is located in a user package. In other words, package user now contains both controller and model functions, for which the controller related functions rely on datastore package, while the DSDatabase interface rely on the user model functions.

I would really appreciate help figuring out how to overcome the import cycle while keeping the DSDatastore interface separate from all the other packages such as home and user.


in case the above is not clear enough, the above code has changed to:

package datastore

import (
    "github.com/username/projectname/user"
)

type DSDatabase interface {
    user.CreateUser(ctx context.Context, username string, passwoUserRegister(ctx context.Context, username string, password []byte) (*datastore.Key, error)
}

...

while in my user package I have this in a controller-related file:

package user

import (
    "github.com/username/projectname/datastore"
)

func CreateUserPOST(w http.ResponseWriter, r *http.Request) {
    // get formdata and such
    datastore.DB.CreateUser(ctx, "username", []byte("password"))
}

and in another model-related file I have:

package user

import (
    "github.com/username/projectname/datastore"
)

func (db *datastore.datastoreDB) CreateUser(ctx context.Context, username string) (*User, error) {
    key := datastore.NameKey("User", username, nil)
    var user User
    err := db.client.Get(ctx, key, &user)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

Which of course results in an import cycle, that I sadly can't figure out how to overcome..

fisker
  • 979
  • 4
  • 18
  • 28
  • 1
    You seem to be looking for a technical solution, but really you need to better define which package uses which. With the terminology, it feels like the datastore would be the lowest level and wouldn't know about other packages. But I can't really tell the business logic you have. Also, it feels weird to have a the datastoreDB receiver in the user package. Perhaps this method should be in the datastore package ? Perhaps the code should just be in the same package ? If you look at real-life projects, they have tons of files within the same packages. – alexbt Apr 08 '17 at 22:28
  • If datastore and user both require each other, then they simply can't be separate packages. Why are they in different packages in the first place? – JimB Apr 08 '17 at 23:06
  • @alexbt Before I used a MVC structure, but I liked the idea of putting controllers and models together [as recommended in my earlier question](https://stackoverflow.com/questions/43295691/identical-package-names-in-different-folders-for-same-project).. the datastore.go file also contain configuration to create a single datastore client that I can then access using the datastoreDB receiver.. but it sounds like I need to go back and divide `user model functions` and `user controller functions` to separate packages? – fisker Apr 08 '17 at 23:13
  • @jimb I put them in separate packages because I felt it was necessary to separate all the functions (that had nothing to do with each other) in the `models` and `controllers` packages, and I was recommended to put everything that was related to `users` together to avoid having the directories: `controllers/user` and `models/user` which made sense to me – fisker Apr 08 '17 at 23:18
  • Does this answer your question? [Import cycle not allowed](https://stackoverflow.com/questions/28256923/import-cycle-not-allowed) – Michael Freidgeim Apr 24 '21 at 08:36

1 Answers1

4

First things first, you cannot define a method, in pacakge A, on a type declared in package B.

So this...

package user

import (
    "github.com/username/projectname/datastore"
)

func (db *datastore.datastoreDB) CreateUser(ctx context.Context, username string) (*User, error) {
    key := datastore.NameKey("User", username, nil)
    var user User
    err := db.client.Get(ctx, key, &user)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

...should not even compile.

This here...

package datastore

import (
    "github.com/username/projectname/user"
)

type DSDatabase interface {
    user.CreateUser(ctx context.Context, username string, passwoUserRegister(ctx context.Context, username string, password []byte) (*datastore.Key, error)
}

...this is also invalid Go code.


As to your question... one thing you could do is to define the Datastore interface inside the user package and have the implementation live in another package, this lends itself nicely for when you need different implementations of one interface. If you do this your user package does not need to know about the datastore package anymore, the datastore package still has to know about the user package though, which is a OK.

An example:

package user

import (
    "context"
)

type DSDatabase interface {
    CreateUser(ctx context.Context, username string, password []byte) (*User, error)
    // ...
}

// This can be set by the package that implements the interface
// or by any other package that imports the user package and
// a package that defines an implementation of the interface.
var DB DSDatabase

type User struct {
    // ...
}

func CreateUserPOST(w http.ResponseWriter, r *http.Request) {
    // get formdata and such
    DB.CreateUser(ctx, "username", []byte("password"))
}

The package with the implementation of the interface:

package datastore

import (
    "context"
    "github.com/username/projectname/user"
)

// DB implements the user.DSDatabase interface.
type DB struct { /* ... */ }

func (db *DB) CreateUser(ctx context.Context, username string) (*user.User, error) {
    key := datastore.NameKey("User", username, nil)
    var user user.User
    err := db.client.Get(ctx, key, &user)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func init() {
    // make sure to initialize the user.DB variable that
    // is accessed by the CreateUserPOST func or else you'll
    // get nil reference panic.
    user.DB = &DB{}
}
mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • This is great, thanks a lot.. But I guess this means that it's not possible to define the CreateUser function inside the user package? The main reason for abandoning the `models` and `controllers` package structure was to put all functions related to the user inside a user package, but it seems like this solution will still put all the models functions inside a `datastore` package? I do love the idea of defining the `DSDatabase` interface inside the `user` package, but is that even possible without importing the `datastore` package? – fisker Apr 09 '17 at 00:49
  • You can very well have the CreateUser func in the user package but then having the DSDatabase interface seems pointless. What do you need it for? – mkopriva Apr 09 '17 at 00:58
  • But if I understand correctly, by putting the CreateUser function inside the `user` package then you need to import the `datastore` package (otherwise you'll end up creating multiple datastore clients), resulting in the import cycle error? I'm working on a webapp that's already quite big and I end up naming my functions with specific prefixes because all the files are located inside `controllers` and `models` packages, so I was looking for a way to split it up – fisker Apr 09 '17 at 01:05
  • If you define `CreateUserPOST` and `CreateUser` inside the `user` package, which is ok, what do you need the extra `datastore` package for? What is the purpose of the `DSDatabase` interface? – mkopriva Apr 09 '17 at 01:18
  • The purpose is to have a single datastore client, and I believe to achieve this the DSDatabase interface is necessary? With the previous `models` package structure then functions related to `users`, `posts`, etc. could easily use the same datastore client; but how to achieve this if you have `package user` and `package post` containing both controller and model functions? – fisker Apr 09 '17 at 01:30
  • You don't need an interface for that, you can just have a single instance of the client set to a variable inside a package, let's say your `datastore` package, and that's it... `user` and `post` can import `datastore` to use the client, and `datastore` only needs to import the `cloud.google.com/go/datastore` package to be able to declare the variable. – mkopriva Apr 09 '17 at 01:41
  • Oh.. I saw google set it up it like this in one of their GitHub repos and figured it was necessary.. I'll try to make it work as you described.. thanks for everything :) – fisker Apr 09 '17 at 01:58