This is a follow on from Calling a function with Go Reflect .
To simplify the question I cut out what I could, hard coded some values and ~hopefully~ didn't make it unclear in the process. I'm getting an error on the code "method.Call(env)" near the bottom.
Ideally what I would like to do is minimize the use of reflection similarly how ThunderCat did on the previous question with the line:
method := miType.Method(i).Func.Interface().(func(core.ModuleInfo) core.ModuleInfo)
but if that's not possible the simplest way it can be done would be perfectly fine. If this seems like a basic question, my apologies, I am very new to Go.
The error I am getting is:
cannot use env (type Environment) as type []reflect.Value in argument to method.Call
which is because I would like to assert the method to the function with the correct signature as was done on the previous quesiton but after quite a bit of playing around I just haven't quite got it.
The simplified code:
package main
import (
"flag"
"fmt"
"reflect"
)
type CommandLineFlags struct {
Debug *bool
}
type Environment struct {
CLF CommandLineFlags
}
type ModuleInfo struct {
Initialize bool // Flag: True of module has Initialization function and it should be called. Default: false
Module string // Name of the module. No need to hard code, will be set during initialization.
}
type ModuleInit struct{}
func main() {
var env Environment
env.CLF.Debug = flag.Bool("dbg", false, "Enables Debug Messages")
flag.Parse()
modules := make([]ModuleInfo, 1)
modules[0].Initialize = true
modules[0].Module = "logger"
miValue := reflect.ValueOf(ModuleInit{})
// miType := reflect.TypeOf(ModuleInit{})
for _, m := range modules {
if m.Initialize {
funcName := m.Module + "Init"
method := miValue.MethodByName(funcName)
fmt.Println(funcName)
// Would like to do something like this
// ...Func.Interface().(func(core.ModuleInit) core.ModuleInit)
// like is done with the referenced quesiton above so as to minimize the use of reflect calls.
method.Call(env)
}
}
}
func (mi ModuleInit) LoggerInit(env *Environment) {
var debugEnabled = *env.CLF.Debug
// ...and more stuff.
}
The method has the type func(*Environment). Assert to that type and call:
modules := make([]ModuleInfo, 1)
modules[0].Initialize = true
modules[0].Module = "Logger"
miValue := reflect.ValueOf(ModuleInit{})
for _, m := range modules {
if m.Initialize {
funcName := m.Module + "Init"
method := miValue.MethodByName(funcName).Interface().(func(*Environment))
method(&env)
}
}
Run it on the Playground.
(Note two issues fixed: The module should be "Logger", not "logger", method takes a *Environment, not an Environment.)
The code above will panic if the method is not found or does not have the correct type. Here's the code with checks to prevent a panic:
modules := make([]ModuleInfo, 1)
modules[0].Initialize = true
modules[0].Module = "Logger"
miValue := reflect.ValueOf(ModuleInit{})
for _, m := range modules {
if m.Initialize {
funcName := m.Module + "Init"
method := miValue.MethodByName(funcName)
if !method.IsValid() {
fmt.Printf("method %s not found", funcName)
continue
}
fn, ok := method.Interface().(func(*Environment))
if !ok {
fmt.Println("method is not func(*Environment)")
continue
}
fn(&env)
}
}
Run it on the Playground.
There is several errors in OP code,
the func name was not properly generated,
the reflected method instance was not properly checked for validity,
env parameter of LoggerInit was a pointer, a value was sent in,
method call was not properly done.
Here is the fixed version (https://play.golang.org/p/FIEc6bTvGWJ).
package main
import (
"flag"
"fmt"
"log"
"reflect"
"strings"
)
type CommandLineFlags struct {
Debug *bool
}
type Environment struct {
CLF CommandLineFlags
}
type ModuleInfo struct {
Initialize bool // Flag: True of module has Initialization function and it should be called. Default: false
Module string // Name of the module. No need to hard code, will be set during initialization.
}
type ModuleInit struct{}
func main() {
var env Environment
env.CLF.Debug = flag.Bool("dbg", false, "Enables Debug Messages")
flag.Parse()
modules := make([]ModuleInfo, 1)
modules[0].Initialize = true
modules[0].Module = "logger"
miValue := reflect.ValueOf(ModuleInit{})
// miType := reflect.TypeOf(ModuleInit{})
for _, m := range modules {
if m.Initialize {
funcName := strings.Title(m.Module) + "Init"
method := miValue.MethodByName(funcName)
log.Printf("%#v %v\n", method, funcName)
if !method.IsValid() || method.IsNil() {
break
}
fmt.Println(funcName)
// Would like to do something like this
// ...Func.Interface().(func(core.ModuleInit) core.ModuleInit)
// like is done with the referenced quesiton above so as to minimize the use of reflect calls.
out := method.Call([]reflect.Value{reflect.ValueOf(env)})
fmt.Println(out) // A bunch of relfect.Values.
}
}
}
func (mi ModuleInit) LoggerInit(env Environment) {
var debugEnabled = *env.CLF.Debug
// ...and more stuff.
log.Println("LoggerInit ", debugEnabled)
}
The issue is that the argument passed reflect.Value.Call needs to be of the type reflect.Value itself. See the signature from https://golang.org/pkg/reflect/#Value.Call
func (v Value) Call(in []Value) []Value
You must wrap the env variable in a []reflect.Value since reflect.Value.Call requires a slice of reflect.Value.
args := []reflect.Value{reflect.ValueOf(&env),}
method.Call(args)
Also, some typos in your code:
modules[0].Module = "Logger"
I'm new to golang and golang plugins. I'm having trouble passing a chan object to this functions. If I switch to int it works. Not sure what exactly I am missing. Thanks for any help.
dataunit.go
package main
type DataUnit struct {
i int
s string
}
modcounter.go
package main
import (
"fmt"
// "strconv"
)
type module string
//func (m module) RunMod(i int) {
func (m module) RunMod(in <-chan *DataUnit) {
//fmt.Println("Hello Universe " + strconv.Itoa(i))
fmt.Println("Hello Universe ")
n := <-in
fmt.Printf(n.s)
}
var Module module
modmain.go
package main
import (
"fmt"
"os"
"plugin"
)
type DataUnit struct {
i int
s string
}
type Module interface {
//RunMod(i int)
RunMod(in <-chan *DataUnit)
}
func main() {
out := make(chan *DataUnit, 2000)
plug, err := plugin.Open("./modcounter.so")
if err != nil {
fmt.Printf("FATAL (plugin.Open): " + err.Error())
os.Exit(1)
}
symModule, err := plug.Lookup("Module")
if err != nil {
fmt.Printf(err.Error())
os.Exit(1)
}
var module Module
module, ok:= symModule.(Module)
if !ok {
fmt.Println("unexpected type from module symbol")
os.Exit(1)
}
//module.RunMod(5)
module.RunMod(out)
}
go build -buildmode=plugin -o modcounter.so modcounter.go dataunit.go
go build modmain.go dataunit.go
./modmain
unexpected type from module symbol
If you are still learning golang then plugins are definitely not the place to start. I believe plugin support is still experimental and doesn't work on all OS - in 1.10 I believe Darwin was added to previous Linux.
A quick search reveals a pre-existing issue reported similar to yours -looks like it's in go 1.10:
https://github.com/golang/go/issues/24351
You can maybe track through the repository and get a branch or version where this is fixed, but whilst your learning you aren't going to have any confidence in whether problems with this area of functionality are with your code or the plugin system. I recommend therefore sticking to learning go with core language features for now.
In go is possible to pass channels of objects!!
For example, if you to use <-chan http.Header will works fine. The question is that the params must be shared between modules and application. So if you reallocate DataUnit for another package will work.
My test was structured like:
My interface:
//in modmain.go
type Module interface {
RunMod(in <-chan *mydata.DataUnit)
}
My module:
//in modcounter.go
func (m module) RunMod(in <-chan *mydata.DataUnit) {
fmt.Println("Hello Universe ")
n := <-in
fmt.Printf("%v", n.S)
}
My data:
//in dataunit.go
type DataUnit struct {
I int //export field
S string //export field
}
The result:
P.S.: Docker with golang 1.10 was used for tests.
#in Dockerfile
FROM golang:1.10
COPY . /go/
RUN export GOPATH=$GOPATH:/go/
RUN cd /go/src/mydata && go build dataunit.go
RUN cd /go/src/app && go build modmain.go
RUN cd /go/src/app && go build -buildmode=plugin -o modcounter.so modcounter.go
WORKDIR /go/src/app
RUN ls -l
CMD ["/go/src/app/modmain"]
I'm running into a Go behaviour which I don't understand. My idea is to import a plugin which implements an interface that is out of both packages. If a struct is returned it works fine, but to be sure it implements the interface, I want to return an interface which fails.
Interface definition:
package iface
type IPlugin interface{
SayHello(string)
SayGoodby(string)
WhatsYourName() string
}
The main program looks like this:
package main
import (
"plugin"
"plugin_test/iface"
"errors"
"fmt"
)
//go:generate go build -buildmode=plugin -o ./pg/test.so ./pg/test.go
func main(){
path := "pg/test.so"
plug, err := plugin.Open(path)
if err != nil {
panic(err)
}
sym, err := plug.Lookup("Greeter")
if err != nil {
panic(err)
}
var pg iface.IPlugin
pg, ok := sym.(iface.IPlugin)
if !ok {
panic(errors.New("error binding plugin to interface"))
}
fmt.Printf("You're now connected to: %s \n", pg.WhatsYourName())
pg.SayHello("user")
pg.SayGoodby("user")
}
The plugin (stored in pg/test.go)
package main
import (
"fmt"
"plugin_test/iface"
)
type testpl struct {}
func(pl testpl) SayHello(s string){
fmt.Printf("Plugin says hello to %s \n", s)
}
func(pl testpl) SayGoodby(s string){
fmt.Printf("Plugin says goodby to %s \n", s)
}
func(pl testpl) WhatsYourName() string{
return "my name is Test-Plugin"
}
/* This function works */
func getPlugin() testpl{
return testpl{}
}
/* This function doesn't work */
func getPlugin() iface.IPlugin{
return testpl{}
}
/* This function also doesn't work */
func getPlugin() interface{}{
return testpl{}
}
var Greeter = getPlugin()
I tried every getPlugin function on its own.
The function returning testpl prints the expected output:
You're now connected to: my name is Test-Plugin
Plugin says hello to user
Plugin says goodby to user
The other functions end on sym.(iface.IPlugin)
panic: error binding plugin to interface
goroutine 1 [running]:
main.main()
/home/../../../main.go:27 +0x343
exit status 2
Can someone explain why this isn't possible? Wouldn't it be easier to create a plugin if it did't let you build your plugin in such a case?
What you want is possible, but there is something in the background that prevents it from working.
This is namely that you want to lookup a variable named Greeter from the plugin. Plugin.Lookup() will return a pointer to this variable! If it wouldn't, you could only inspect its value, but you couldn't change it.
You can verify this by simply printing the type of the value stored in sym:
fmt.Printf("%T\n", sym)
In your first case func getPlugin() testpl, output will be:
*main.testpl
In your second case func getPlugin() iface.IPlugin, output will be:
*iface.IPlugin
(Yes, it's a pointer to an interface!)
In your third case func getPlugin() interface{}, output will be:
*interface {}
So your first example works because the value stored in sym is of type *main.testpl, which also implements iface.IPlugin (because main.testpl implements it, so does the pointer type).
Back to your 2nd example: func getPlugin() iface.IPlugin
The value stored in sym is of type *iface.IPlugin. A value of pointer type to interface never satisfies any interfaces (except the empty interface), so attempting to type-assert iface.IPlugin from a value of type *iface.IPlugin will never succeed. You have to type assert *iface.IPlugin type, which you can dereference after to obtain a value of type iface.IPlugin. It could look like this:
pgPtr, ok := sym.(*iface.IPlugin)
if !ok {
panic(errors.New("error binding plugin to interface"))
}
pg := *pgPtr
And now everything works as expected!
To avoid such hassle and confusion, you may implement your plugin to expose a function which returns you the Greeter:
func Greeter() iface.IPlugin { return testpl{} }
And then get rid of the Greeter global var of course. If you do this, you may lookup the Greeter symbol which will be of type:
func() iface.IPlugin
The difference is that looking up a function does not require the plugin package to return a pointer to the value, while in case of a variable it does. A simple function type, no pointer-to-interface-kung-fu. Using it to obtain the greeter would be:
Greeter, err := p.Lookup("Greeter")
if err != nil {
panic(err)
}
greeterFunc, ok := GetFilter.(func() iface.IPlugin)
if !ok {
panic(errors.New("not of expected type"))
}
greeter := greeterFunc()
// And using it:
fmt.Printf("You're now connected to: %s \n", greeter.WhatsYourName())
greeter.SayHello("user")
greeter.SayGoodby("user")
See related / similar question: go 1.8 plugin use custom interface
I need to create builder (base) and specific builders for each build type.
e.g.
builder for html project
builder for node.js project
builder for python project
builder for java project
….
The main functionality will be like following:
File:Builder.go
interface
type Builder interface {
Build(string) error
}
File: nodebuilder.go
//This is the struct ???? not sure what to put here...
type Node struct {
}
func (n Node) Build(path string) error {
//e.g. Run npm install which build's nodejs projects
command := exec.Command("npm", "install")
command.Dir = “../path2dir/“
Combined, err := command.CombinedOutput()
if err != nil {
log.Println(err)
}
log.Printf("%s", Combined)
}
...
//return new(error)
}
Main assumptions/process:
To start build on each module I need to get the path to it
I need to copy the module to a temp folder
I need to run the build on it (implement the build interface like mvn build npm install etc)
After build was finished zip the module with dep
Copy it to new target folder
Note: beside from the build and the path (that should be handled specifically ) all other functionality are identical
like zip copy
Where should I put the the zip and copy (in the structure) and for example how should I implement them and path them to the builder ?
Should I structure the project differently according the assumptions?
The first principle of SOLID says that a piece of code should have only one responsibility.
Takes the context in, it really makes no sense that any builder to care about the copy and zip part of the build process. It's beyond builder's responsibility. Even using composition (embedding) is not neat enough.
Narrow it down, the Builder's core responsibility is to build the code, as the name suggests. But more specifically, the Builder's responsibility is to build the code at a path. What path? The most idomatic way is the current path, the working dir. This adds two side methods to the interface: Path() string which returns the current path and ChangePath(newPath string) error to change the current path. The implentation would be simple, preserve a single string field as the current path would mostly do the job. And it can easily be extended to some remote process.
If we looks it carefully, there auctually is two build concepts. One is the whole building process, from making temp dir to copy it back, all five steps; the other is the build command, which is the third step of the process.
That is very inspiring. A process is idomatic to be presented as a function, as classic procedural programing would do. So we write a Build function. It serialize all the 5 steps, plain and simple.
Code:
package main
import (
"io/ioutil"
)
//A builder is what used to build the language. It should be able to change working dir.
type Builder interface {
Build() error //Build builds the code at current dir. It returns an error if failed.
Path() string //Path returns the current working dir.
ChangePath(newPath string) error //ChangePath changes the working dir to newPath.
}
//TempDirFunc is what generates a new temp dir. Golang woould requires it in GOPATH, so make it changable.
type TempDirFunc func() string
var DefualtTempDirFunc = func() string {
name,_ := ioutil.TempDir("","BUILD")
return name
}
//Build builds a language. It copies the code to a temp dir generated by mkTempdir
//and call the Builder.ChangePath to change the working dir to the temp dir. After
//the copy, it use the Builder to build the code, and then zip it in the tempfile,
//copying the zip file to `toPath`.
func Build(b Builder, toPath string, mkTempDir TempDirFunc) error {
if mkTempDir == nil {
mkTempDir = DefaultTempDirFunc
}
path,newPath:=b.Path(),mkTempDir()
defer removeDir(newPath) //clean-up
if err:=copyDir(path,newPath); err!=nil {
return err
}
if err:=b.ChangePath(newPath) !=nil {
return err
}
if err:=b.Build(); err!=nil {
return err
}
zipName,err:=zipDir(newPath) // I don't understand what is `dep`.
if err!=nil {
return err
}
zipPath:=filepath.Join(newPath,zipName)
if err:=copyFile(zipPath,toPath); err!=nil {
return err
}
return nil
}
//zipDir zips the `path` dir and returns the name of zip. If an error occured, it returns an empty string and an error.
func zipDir(path string) (string,error) {}
//All other funcs is very trivial.
Most of things are covered in the comments and I am really felling lazy to write all those copyDir/removeDir things. One thing that is not mentioned in the design part is the mkTempDir func. Golang would be unhappy if the code is in /tmp/xxx/ as it is outside the GOPATH, and it would make more trouble to change GOPATH as it will break import path serching, so golang would require a unique function to generate a tempdir inside the GOPATH.
Edit:
Oh, one more thing I forgot to say. It is terribly ugly and irresponsible to handle errors like this. But the idea is there, and more decent error handling mostly requires the usage contents. So do change it yourself, log it, panic or whatever you want.
Edit 2:
You can re-use your npm example as following.
type Node struct {
path string
}
func (n Node) Build(path string) error {
//e.g. Run npm install which build's nodejs project
command := exec.Command("npm", "install")
command.Dir = n.path
Combined, err := command.CombinedOutput()
if err != nil {
log.Println(err)
}
log.Printf("%s", Combined)
return nil
}
func (n *Node) ChangePath(newPath string) error {
n.path = newPath
}
func (n Node) Path() string {
return n.path
}
And to combine it with other language all together:
func main() {
path := GetPathFromInput()
switch GetLanguageName(path) {
case "Java":
Build(&Java{path},targetDirForJava(),nil)
case "Go":
Build(&Golang{path,cgoOptions},targetDirForGo(),GoPathTempDir()) //You can disable cgo compile or something like that.
case "Node":
Build(&Node{path},targetDirForNode(),nil)
}
}
One trick is get language name. GetLanguageName should return the name of the language the code in path is using. This can be done by using ioutil.ReadDir to detect filenames.
Also note that although I made the Node struct very simple and only stores a path field, you can extend it easily. Like in Golang part, you might add build options there.
Edit 3:
About package structure:
First of all, I think literally everything: the Build function, language builders and other util/helpers should be put into a single package. They all work for a single task: build a language. There is no need and hardly any expectation to isolate any piece of code as another (sub)package.
So that means one dir. The remaining is really some very personal style but I will share mine:
I would put the function Build and interface Builder into a file called main.go. If the front-end code is minimal and very readable, I would put them into main.go as well, but if it is long and have some ui-logic, I would put it into a front-end.go or cli.go or ui.go, depending on the auctual code.
Next, for each language, I would create a .go file with the language code. It makes clear where I can check them. Alternatively, if the code is really tiny, it is not a bad idea to put them all together into a builders.go. After all, mordern editors can be more than capable to get definitaion of structs and types.
Finally, all the copyDir, zipDir functions goes to a util.go. That is simple - they are utilities, most time we just don't want to bother them.
Go is not an object-oriented language. This means that by design, you don't necessarily have all the behaviour of a type encapsulated in the type itself. And this is handy when you think that we don't have inheritance.
When you want to build a type upon another type, you use composition instead: a struct can embed other types, and expose their methods.
Let's say that you have a MyZipper type that exposes a Zip(string) method, and a MyCopier that exposes a Copy(string) method:
type Builder struct {
MyZipper
MyCopier
}
func (b Builder) Build(path string) error {
// some code
err := b.Zip(path)
if err != nil {
return err
}
err := b.Copy(path)
if err != nil {
return err
}
}
An this is composition in Go. Going further, you can even embed non-exposed types (e.g. myZipper and myCopier) if you only want them to be called from within the builder package. But then, why embedding them in the first place?
You can chose between several different valid designs for your Go project.
solution 1: Single package exposing multiple Builder types
In this case, you want a builder package, that will expose multiple builders.
zip and copy are two functions defined somewhere in the package, they don't need to be methods attached to a type.
package builder
func zip(zip, args string) error {
// zip implementation
}
func cp(copy, arguments string) error {
// copy implementation
}
type NodeBuilder struct{}
func (n NodeBuilder) Build(path string) error {
// node-specific code here
if err := zip(the, args); err != nil {
return err
}
if err := cp(the, args); err != nil {
return err
}
return nil
}
type PythonBuilder struct{}
func (n PythonBuilder) Build(path string) error {
// python-specific code here
if err := zip(the, args); err != nil {
return err
}
if err := cp(the, args); err != nil {
return err
}
return nil
}
solution 2: single package, single type embedding specific behaviour
Depending on the complexity of the specific behaviour, you may not want to change the whole behaviour of the Build function, but just to inject a specific behaviour:
package builder
import (
"github.com/me/myproj/copier"
"github.com/me/myproj/zipper"
)
type Builder struct {
specificBehaviour func(string) error
}
func (b Builder) Build(path string) error {
if err := specificBehaviour(path); err != nil {
return err
}
if err := zip(the, args); err != nil {
return err
}
if err := copy(the, args); err != nil {
return err
}
return nil
}
func nodeSpecificBehaviour(path string) error {
// node-specific code here
}
func pythonSpecificBehaviour(path string) error {
// python-specific code here
}
func NewNode() Builder {
return Builder{nodeSpecificBehaviour}
}
func NewPython() Builder {
return Builder{pythonSpecificBehaviour}
}
solution 3: one package per specificBuilder
On the other end of the scale, depending on the package granularity you want to use in your project, you may want to have a distinct package for every builder. With this premise, you want to generalise the shared functionality enough to give it a citizenship as package too. Example:
package node
import (
"github.com/me/myproj/copier"
"github.com/me/myproj/zipper"
)
type Builder struct {
}
func (b Builder) Build(path string) error {
// node-specific code here
if err := zipper.Zip(the, args); err != nil {
return err
}
if err := copier.Copy(the, args); err != nil {
return err
}
return nil
}
solution 4: functions!
If you know your builders will be purely functional, meaning they don't need any internal state, then you may want your Builders to be function types intead of interfaces. You will still be able to manipulate them as a single type from the consumer side, if this is what you want:
package builder
type Builder func(string) error
func NewNode() Builder {
return func(string) error {
// node-specific behaviour
if err := zip(the, args); err != nil {
return err
}
if err := copy(the, args); err != nil {
return err
}
return nil
}
}
func NewPython() Builder {
return func(string) error {
// python-specific behaviour
if err := zip(the, args); err != nil {
return err
}
if err := copy(the, args); err != nil {
return err
}
return nil
}
}
I wouldn't go with functions for your particular case, because you will need to solve very different problems with every BUilder, and you will definitely need some state at some point.
... I'll leave you the pleasure to combine together some of these techniques, if you are having a boring afternoon.
Bonus!
Don't be afraid of creating multiple packages, as this will help you design clear boundaries between the types, and take full advantage of encapsulation.
The error keyword is an interface, not a type! You can return nil if you have no errors.
Ideally, you don't define the Builder interface in the builder package: you don't need it. The Builder interface will sit in the consumer package.
Let's go through each question one by one:
1. Where should I put the the zip and copy (in the structure) and for example how should I implement them and path them to the builder ?
An interface does not carry any data (assuming you wanted to implement one from your code). It is just a blueprint an object can implements in order to pass as a more generic type. In this case, if you are not passing Builder type anywhere, the interface is redundant.
2. Should I structure the project differently according the assumptions?
This is my take on the project. I'll explain each part separately after the code:
package buildeasy
import (
"os/exec"
)
// Builder represents an instance which carries information
// for building a project using command line interface.
type Builder struct {
// Manager is a name of the package manager ("npm", "pip")
Manager string
Cmd string
Args []string
Prefn func(string) error
Postfn func(string) error
}
func zipAndCopyTo(path string) error {
// implement zipping and copy to the provided path
return nil
}
var (
// Each manager specific configurations
// are stored as a Builder instance.
// More fields and values can be added.
// This technique goes hand-in-hand with
// `wrapBuilder` function below, which is
// a technique called "functional options"
// which is considered a cleanest approach in
// building API methods.
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
NodeConfig = &Builder{
Manager: "npm",
Postfn: zipAndCopyTo,
}
PythonConfig = &Builder{
Manager: "pip",
Postfn: zipAndCopyTo,
}
)
// This enum is used by factory function Create to select the
// right config Builder from the array below.
type Manager int
const (
Npm Manager = iota
Pip
// Yarn
// ...
)
var configs = [...]*Builder{
NodeConfig,
PythonConfig,
// YarnConfig,
}
// wrapBuilder accepts an original Builder and a function that can
// accept a Builder and then assign relevant value to the first.
func wrapBuilder(original *Builder, wrapperfn func(*Builder)) error {
if original != nil {
wrapperfn(original)
return nil
}
return errors.New("Original Builder is nil")
}
func New(manager Manager) *Builder {
builder := new(Builder)
// inject / modify properties of builder with relevant
// value for the manager we want.
wrapBuilder(builder, configs[int(manager)])
})
return builder
}
// Now you can have more specific methods like to install.
// notice that it doesn't matter what this Builder is for.
// All information is contained in it already.
func (b *Builder) Install(pkg string) ([]byte, error) {
b.Cmd = "install"
// if package is provided, push its name to the args list
if pkg != "" {
b.Args = append([]string{pkg}, b.Args...)
}
// This emits "npm install [pkg] [args...]"
cmd := exec.Command(b.Manager, (append([]string{b.Cmd}, b.Args...))...)
// default to executing in the current directory
cmd.Dir = "./"
combined, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
return combined, nil
}
func (b *Builder) Build(path string) error {
// so default the path to a temp folder
if path == "" {
path = "path/to/my/temp"
}
// maybe prep the source directory?
if err := b.Prefn(path); err != nil {
return err
}
// Now you can use Install here
output, err := b.Install("")
if err != nil {
return err
}
log.Printf("%s", output)
// Now zip and copy to where you want
if err := b.Postfn(path); err != nil {
return err
}
return nil
}
Now this Builder is generic enough to handle most build commands. Notice Prefn and Postfn fields. These are hook functions you can run before and after the command runs within Build. Prefn can check if, say, the package manager is installed and install it if it's not (or just return an error). Postfn can run your zip and copy operations, or any clean up routine. Here's a usecase, provided superbuild is our fictional package name and the user is using it from outside:
import "github.com/yourname/buildeasy"
func main() {
myNodeBuilder := buildeasy.New(buildeasy.NPM)
myPythonBuilder := buildeasy.New(buildeasy.PIP)
// if you wanna install only
myNodeBuilder.Install("gulp")
// or build the whole thing including pre and post hooks
myPythonBuilder.Build("my/temp/build")
// or get creative with more convenient methods
myNodeBuilder.GlobalInstall("gulp")
}
You can predefine a few Prefns and Postfns and make them available as option for the user of your program, assuming it's a command line program, or if it's a library, have the user write them herself.
wrapBuilder function
There are a few techniques used in constructing an instance in Go. First, parameters can be passed into a constructor function (this code is for explanation only and not to be used):
func TempNewBuilder(cmd string) *Builder {
builder := new(Builder)
builder.Cmd = cmd
return builder
}
But this approach is very ad-hoc because it is impossible to pass arbitrary values to configure the returned *Builder. A more robust approach is to pass a config instance of *Builder:
func TempNewBuilder(configBuilder *Builder) *Builder {
builder := new(Builder)
builder.Manager = configBuilder.Manager
builder.Cmd = configBuilder.Cmd
// ...
return builder
}
By using wrapBuilder function, you can write a function to handle (re)assigning of values of an instance:
func TempNewBuilder(builder *Builder, configBuilderFn func(*Builder)) *Builder {
configBuilderFn(builder)
}
Now you can pass in any function for configBuilderFn to configure your *Builder instance.
To read more, see https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.
configs array
configs array goes hand-in-hand with the enum of Manager constants. Take a look at New factory function. the enum constant manager passed in pass the parameter is type Manager which is just an int underneath. This means all we had to do is access configs using the manager as the index in wrapBuilder:
wrapBuilder(builder, configs[int(manager)])
For instance, if manager == Npm,configs[int(manager)] will return NodeConfig from configs array.
Structuring package(s)
At this point, it is fine to have zip and copy functions to live in the same package as Build the way I did. There's little use in prematurely optimizing anything or worry about that until you have to. That will only introduce more complexity than you'd want. Optimization comes consistently as you develop the code.
If you feel like structuring the project early is important, you can do it based on the semantic of your API. For instance, to create a new *Builder, it is quite intuitive for the user to call a factory function New or Create from a subpackage buildeasy/builder:
// This is a user using your `buildeasy` package
import (
"github.com/yourname/buildeasy"
"github.com/yourname/buildeasy/node"
"github.com/yourname/buildeasy/python"
)
var targetDir = "path/to/temp"
func main() {
myNodeBuilder := node.New()
myNodeBuilder.Build(targetDir)
myPythonBuilder := python.New()
myPythonBuilder.Install("tensorflow")
}
Another more verbose approach is to include the semantic as part of the function's name, which is also used in Go's standard packages:
myNodeBuilder := buildeasy.NewNodeBuilder()
myPythonBuilder := buildeasy.NewPipBuilder()
// or
mySecondNodeBuilder := buildeasy.New(buildeasy.Yarn)
In Go's standard packages, verbose functions and methods are common. That's because it normally structure sub-packages (subdirectories) for more specific utilities such as path/filepath, which contains utility functions related to file path manipulation while keeping path's API basic and clean.
Coming back to your project, I would keep most common, more generic functions at the top level directory/package. This is how I would tackle the structure:
buildeasy
├── buildeasy.go
├── python
│ └── python.go
└── node/
└── node.go
While package buildeasy contains functions like NewNodeBuilder, NewPipBuilder, or just New that accepts additional options (like the above code), in subpackage buildeasy/node, for instance, can look like this:
package node
import "github.com/yourname/buildeasy"
func New() *buildeasy.Builder {
return buildeasy.New(buildeasy.Npm)
}
func NewWithYarn() *buildeasy.Builder {
return buildeasy.New(buildeasy.Yarn)
}
// ...
or buildeasy/python:
package python
import "github.com/yourname/buildeasy"
func New() *buildeasy.Builder {
return buildeasy.New(buildeasy.Pip)
}
func NewWithEasyInstall() *buildeasy.Builder {
return buildeasy.New(buildeasy.EasyInstall)
}
// ...
Note that in the subpackages you never have to call buildeasy.zipAndCopy because it is a private function that is lower level than the node and python subpackages should care. these subpackages act like another layer of API calling buildeasy's functions and passing in some specific values and configurations that make life easier for the user of its API.
Hope this makes sense.