I assume your main_package
isn't the main
package in Go. I think the child_packages shouldn't be under the main_package
as our goal is to decouple each package from one another.
This is the pattern that I'm currently using in my project to avoid dependency conflicts:
project/
├── main_package
│ └── main_package.go
├── brokers
│ └── brokers.go
├── child_package1
│ └── child_package1.go
├── child_package2
│ └── child_package2.go
└── child_package3
└── child_package3.go
Essentially, each package should never have to deal with anything outside of itself (or at least do so with as little as possible). The broker
will be the sole party who "negotiates" between any two packages.
// main_package.go
package main_package
import (
"path/to/sql"
"path/to/mux"
"path/to/brokers"
)
// Never use selectors from packages directly
// but create a `Broker` object for each endpoint
var bk1 = brokers.New("/api1")
var bk2 = brokers.New("/api2")
var bk3 = brokers.New("/api3")
func Handlers(db *sql.DB, customeruploadFile string) *mux.Router {
router := mux.NewRouter()
// each broker has its own `MyHandler` function
router.HandleFunc("/api1", bk1.MyHandler)
router.HandleFunc("/api2", bk2.MyHandler)
router.HandleFunc("/api3", bk3.MyHandler)
fileHandler := http.FileServer(http.Dir("./client/compiled"))
router.PathPrefix("/").Handler(http.StripPrefix("/", fileHandler))
return router
}
The brokers
package is the central interface for the communication
// brokers.go
package brokers
import (
"path/to/child_package1"
"path/to/child_package2"
"path/to/child_package3"
"net/http"
)
type Broker interface {
MyHandler(http.ResponseWriter, *http.Request)
}
// Factory function to create a `Broker` instance
func New(uri string) Broker {
if uri == "/api1" {
return Broker( new(child_package1.Delegate) )
} else if uri == "/api2" {
return Broker( new(child_package2.Delegate) )
} else if uri == "/api3" {
return Broker( new(child_package3.Delegate) )
}
return nil
}
Now child_packageX
is no long decoupled to any internal dependency, provided
it expose a "representative" or Delegate
object to talk to the broker.
// child_package1.go
package child_package1
import "net/http"
type Delegate struct {
// Optional parameters can be carried by the Delegate
// to be used in the created Broker anywhere
}
func (d *Delegate) MyHandler(w http.ResponseWriter, r *http.Request) {
// Maybe return a JSON here
}
Each child can have its own MyHandler
that does different things for different api calls, without having to know what endpoints they are serving.
// child_package2
package child_package2
import "net/http"
type Delegate struct {}
func (d *Delegate) MyHandler(w http.ResponseWriter, r *http.Request) {
// Maybe return an XML here
}
The main_package
doesn't import all the child_packageX
, but just the broker
package. You can write a test that imports the broker
package instead of the actual packages, or you can even write another broker for testing.
package test
import (
"testing"
"path/to/main_package"
)
func TestMain(*testing.T) {
// test the routing in `main_package`
}
You're no longer testing a functionality of a handler function, but one of an endpoint exposed by a broker. This encourage you to write generic handler functions and focus on the higher level endpoints.
package test
import (
"testing"
"path/to/broker"
)
func TestGetJSONAlright(*testing.T) {
bk1 := brokers.New("/api1")
// test if I get JSON here
}
func TestGetXMLAlright(*testing.T) {
bk1 := brokers.New("/api2")
// test if I get XML here
}
This, in my opinion, is a powerful pattern since you can write more "generic" handlers and just plug them in to the routes you want.