GopherJS and RPC over HTTP
GopherJS enables web development using Go for both the backend server code and frontend browser code. One of the neat things this allows you to do, which the NodeJS community is all too happy to tell you, is share code between the frontend and backend. However, JavaScript is a dynamic language, and lacks many of the static analysis tools that Go provides. By taking advantage of Go’s static analysis, it is possible to develop HTTP endpoints in the backend and automatically generate frontend code for calling them, using Go models and types end-to-end. This is done with the help of a tool I’ve written called gopherpc.
While the technique shown here could also be applied to plain HTTP handlers, I’m going to instead opt for RPC over HTTP, since it lends itself more easily to the type of static analysis we’re interested in.
RPC Over HTTP
Go’s net/rpc package lays the basic
groundwork for RPC in Go. By defining and registering services with methods
following a well-defined pattern, any new methods are automatically made
available, and the runtime takes care of marshalling data to and from the method
call. Gorilla’s rpc package extends
this idea to work over HTTP, which is very similar, but adds the *http.Request
as a required parameter for defined methods.
Here’s how you define a simple Gorilla RPC server:
package main
import (
"log"
"net/http"
"strings"
"github.com/gorilla/rpc/v2"
"github.com/gorilla/rpc/v2/json"
)
type StringService struct{}
func (s StringService) Upper(r *http.Request, str *string, reply *string) error {
*reply = strings.ToUpper(*str)
return nil
}
func main() {
s := rpc.NewServer()
s.RegisterCodec(json.NewCodec(), "application/json")
s.RegisterService(StringService{}, "")
http.Handle("/rpc", s)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
This registers an RPC endpoint at http://localhost:8080/rpc
with a JSON codec.
To access it, you need to send a specially-formatted POST request to it:
$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"method":"StringService.Upper", "params":["hello"], "id":1}' \
http://localhost:8080/rpc
This command would get the following response:
{"result":"HELLO","error":null,"id":1}
Using with GopherJS
In order to use this RPC service from GopherJS, you can annotate it with a
gopherpc:generate
comment, like this:
// gopherpc:generate
type StringService struct{}
// add method definitions
GopherJS bindings to this service can then be generated with gopherpc
:
$ go get github.com/dradtke/gopherpc/cmd/gopherpc
$ gopherpc -scan <pkg> -o <output>
<pkg>
must reference the package where your RPC services are defined, and
<output>
should be set to the path of the Go file to write. The GopherJS code
to call it then looks like this, where <rpc>
references the package to which
<output>
belongs:
// +build js
package main
import (
"github.com/dradtke/gopherpc/json"
rpc <rpc>
)
func main() {
client := rpc.Client{
URL: "http://localhost:8080/rpc",
Encoding: json.Encoding{},
}
result, err := client.StringService().Upper("hello")
if err != nil {
println("failed to call StringService.Upper: " + err.Error())
} else {
println(result) // should be "HELLO"
}
}
Note how this results in a fully statically-compiled frontend binding for asynchronously calling backend services. If a service method gets renamed, or the types don’t align between the backend and frontend code, it results in a GopherJS compile error.
A more full example can be seen here.