gonode acts as a bridge between Go and node.js. It introduces a way to combine the asynchronous nature of node with the simplicity of concurrency in Go. gonode will in a non-blocking fashion run Go code directly from within your node modules, and asynchronously return results from Go. You can code anything you wish as long as the required communication between Go and node.js can be represented with JSON.
First make sure that you have installed Go and set up your GOPATH as gonodepkg and go-simplejson will be automatically installed to the first path specified in your GOPATH.
Then install gonode by running:
npm install gonode
You should now be all set up and no more commands are required.
Note: Even though the required Go packages are installed automatically with gonode you may find yourself in need of installing or updating them explicitly. In that case do so by running:
go get github.com/jgranstrom/gonodepkg github.com/jgranstrom/go-simplejson
To set up communications you need to initiate the gonode module in node and also start the gonodepkg in Go.
Basic requirements in node.js:
var Go = require('gonode').Go;
var go = new Go({
path : 'gofile.go',
});
go.init(function(err) {
if (err) throw err;
// TODO: Add code to execute commands
go.close();
});
Basic requirements in Go (gofile.go):
package main
import (
gonode "github.com/jgranstrom/gonodepkg"
json "github.com/jgranstrom/go-simplejson"
)
func main() {
gonode.Start(process)
}
func process(cmd *json.Json) (response *json.Json) {
// TODO: Add code for processing commands from node
return cmd
}
You can initialize gonode either by explicitly calling init()
or by settings the option initAtOnce
to true
and optionally provide the initialization callback directly in to Go constructor.
var Go = require('gonode').Go;
var options = {
path : 'gofile.go',
initAtOnce : true,
}
var go = new Go(options, function(err) {
if (err) throw err;
// TODO: Add code to execute commands
go.close();
});
As you can see close()
should be called when you no longer need the Go object and will gracefully end the Go process when all executing commands has finished.
path
: The path of the go-file to execute. (Required)initAtOnce
: Will initialize Go at once when object created iftrue
, and allows initialization callback to be provided in constructor. (Default:false
)maxCommandsRunning
: Specifies the maximum number of commands allowed to be running simultaneously, may impact performance differently depending on Go implementation. (Default:10
)defaultCommandTimeoutSec
: Specifies the default command timeout in seconds to be used when not specified in command options. (Default:5
)cwd
: The working directory of the Go process. (Default: Current working directory of the node process)
Running commands with gonode is really simple, the following is an example presuming go is an initialized Go object:
go.execute({commandText: 'Hello world from gonode!'}, function(result, response) {
if(result.ok) {
console.log('Go responded: ' + response.responseText);
}
});
execute()
accepts a JSON object to be sent to the Go process, and a callback which will be called when Go returns with a result or when the command reaches a timeout limit or is terminated.
result
represents the result of the execution of this command.
response
will contain a JSON object with the result of the response only if result.ok
is set to true
.
result
may have one and only one of the following set to true
:
ok
: The command has executed and responded as expected, response data are inresponse
.timeout
: The command reached a timeout by exceeding the set execution time limit.terminated
: The command has been internally terminated prior to responding. This is set when external errors are raised, such as Go panic.
execute()
returns true
if the command has been registered and eventually will be executed, or false
if the command was ignored either because gonode hasn't been initialized yet, or because gonode is in the process of closing or terminating.
Note that the JSON object to send can contain anything containable in JSON and in arbitrary structure, and the JSON object returned does not have to obey to any structure of the sent object as they are completely independent. The structure of the returned object depends on the Go implementation responding it.
Processing the command in Go is possibly even simpler:
response, m, error := json.MakeMap()
m["error"] = error
if(cmd.Get("commandText").MustString() == "Hello") {
m["responseText"] = "Well hello there!"
} else {
m["responseText"] = "What?"
}
return
}
Each command sent to Go will be delegated to the provided process()
on a new go-routine and cmd
will be a pointer to a Json
object which is a representation of the JSON object received from node.
Each process()
call must return a pointer to a Json
object containing any data to be part of the response back to node.
commandTimeoutSec
: Setting this will override thedefaultCommandTimeoutSec
set for the Go object for a specific command. (Default:defaultCommandTimeoutSec
of the Go object)
Command options can be provided in any call to execute()
as such:
go.execute({text: 'Hello world from gonode!'}, function(result, response) {
if(result.ok) {
console.log('Go responded: ' + response.text);
} else if(result.timeout) {
console.log('Command timed out!');
}
}, {commandTimeoutSec: 60}); // This command will execute for up to one minute before timing out
Since gonode supports arbitrary JSON data between Go and node.js you must be able to interact with the data communicated. The following are methods provided to get the JSON data in usable Go types and can be called on Json
objects:
Get(key string)
: Get the pointer to aJson
object for a specific key. You can recursivelyGet()
through the JSON structure to get to any required data.GetIndex(index int)
: Get the pointer to aJson
object for a index within a JSON array.CheckGet(key string)
: Get the pointer to aJson
object for a specific key together with a possible error.Map()
: Assert theJson
object tomap[string]interface{}
, also returns a possible error.Array()
: Assert theJson
object to[]interface{}
, also returns a possible error.Bool()
: Assert theJson
object tobool
, also returns a possible error.String()
: Assert theJson
object tostring
, also returns a possible error.Float64()
: Assert theJson
object tofloat64
, also returns a possible error.Int()
: Assert theJson
object toint
, also returns a possible error.Int64()
: Assert theJson
object toint64
, also returns a possible error.Bytes()
: Assert theJson
object to[]byte
, also returns a possible error.StringArray()
: Assert theJson
object to[]string
, also returns a possible error.IntArray()
: Assert theJson
object to[]int
, also returns a possible error.MustString(args ...string)
: Assert theJson
object tostring
, a default value can optionally be provided as an argument to be returned if the assertion fails.MustInt(args ...int)
: Assert theJson
object toint
, a default value can optionally be provided as an argument to be returned if the assertion fails.MustFloat64(args ...float64)
: Assert theJson
object tofloat64
, a default value can optionally be provided as an argument to be returned if the assertion fails.
To create a Json
object from Go types some additional methods are provided:
Create(data interface{})
: Create aJson
object with arbitrary data. This can be used to take advantage of astruct
or for example creating aJson
object containing a singleint
or array etc.MakeMap()
: Make aJson
object containing amap[string]interface{}
and return a pointer to theJson
object and the createdmap
.
Example of getting JSON data from a Json
object:
Provided JSON data:
{
"data": {
"array": ["abc", "efg", "klm"],
"number": 716
},
"otherdata": "hello"
}
Assuming we have a Json
object called json
of the above JSON we can get each data as such:
firstString, err := json.Get("data").Get("array").GetIndex(0).String() // "abc"
entireArray, err := json.Get("data").Get("array").StringArray() // ["abc" "efg" "klm"]
number, err := json.Get("data").Get("number").Int() // 716
otherdata := json.Get("otherdata").MustString() // "hello"
Example of creating a Json
object from Go types:
The following code:
arr := []int{1, 3, 7}
numberJson := simplejson.Create(arr)
Would simply generate the following JSON:
[1, 3, 7]
While the code:
json, m := simplejson.MakeMap(arr)
m["array"] = []int{1, 3, 7}
Would generate:
{
"array": [1, 3, 7]
}
This enables you to construct any complex JSON structures needed for communication between Go and node.js. However of course it is recommended to keep the actual communication and complexity of the structures as low as possible to improve performance.
There are two ways of closing gonode:
close()
: Go will be closed when all running commands has finished. No more calls toexecute()
will be allowed after this call, but callbacks for already running commands may still be called. When the callback of the last running command has been returned, Go will close gracefully. Calls to this returntrue
if a close has been scheduled, orfalse
if either Go is not initialized or if a close/termination is already pending. Callingclose()
more than once has no significant meaning.terminate()
: Go will be terminated immediately. No more calls toexecute()
will be allowed after this call, and callbacks for already running commands will be called immediately withresult.terminated
set totrue
. Calls to this returntrue
if a termination has been scheduled, orfalse
if either Go is not initialized or if a termination is already pending. Callingterminate()
more than once has no significant meaning.
Example:
// Execute some long running command
go.execute({text: 'I will run for quite a while!'}, function(result, response) {
if(result.ok) {
console.log('Go responded: ' + response.text);
} else if(result.timeout) {
console.log('Command timed out!');
} else if(result.terminated) {
console.log('Command was terminated!');
}
});
//go.terminate(); // This line would most likely cause the above command to terminate
//go.close(); // This would cause gonode to close after the above command has finished
Important: Always close gonode when you no longer need it, otherwise you will leave Go hanging while waiting for more command to execute. It would waste precious resources and also keep your node process from exiting when you would expect it to.
gonode comes with some error handling concerning the Go process as well as JSON parsing errors. On all errors, except for initialization, gonode will emit the error
event with information regarding the event. Such events are raised for example when a panic occurs within Go or when there are errors parsing JSON. The error object has two properties;
parser
:true
if the error is caused by internal parsing errors, otherwise `false.data
: Contains the actual error data which may be error output from Go possibly including stack trace
Handling these errors is straightforward:
var Go = require('gonode').Go;
var go = new Go({
path : 'gofile.go',
initAtOnce : true,
}, function(err) {
if (err) throw err; // This may be a failure to locate go-file
go.on('error', function(err) {
if(err.parser) {
// Error is coming from internal parser
console.log('Parser error: ' + err.data.toString())
} else {
// External error possible Go panic
console.log('Go error: ' + err.data.toString())
}
});
// TODO: Add code to execute commands
go.close();
});
Important: An external error causing the error event to emit with parser
set to false
will also cause gonode to terminate. That means such errors are fatal and would require gonode to be reinitialized. Also it will cause all running commands to be immediately terminated, i.e. their callbacks will be invoked with result.terminated
set to true
.
Note: a big error output like a stack trace caused by a panic may be split up into several error events containing parts of the total output.
- Improved error handling
- Additional benchmarks and examples