Running gofmt in Browser with 10 Lines of Code (Using GopherJS)
I saw a message in Gophers Slack #general chat that stood out to me:
but running
gofmt
in the browser is, um, hard
The context was that they had a 100%-client-side JavaScript application that produced some Go code, but the Go code wasn't gofmt
ed.
"Wait a minute, that's not hard," I thought. "It's trivial! Getting gofmt
to run in the browser? I bet I could do it in 10 lines of code!"
Right?
Then I realized. It's not trivial. It's not obvious. It just seems that way to me because I've done a lot of things like this. But many people probably haven't.
So that inspired me to write this blog post showing how you, too, can have gofmt
functionality running in the browser alongside your JavaScript code very easily. The only thing I'll assume is you're a Go user and have a working Go installation (otherwise, you're unlikely to be interested in gofmt
).
gofmt
has relatively complex behavior, so how can it all be implemented in just 10 lines? The trick is that we don't have to rewrite it all from scratch in JavaScript. Instead, we can write it in Go, as that'll be much easier.
To get Go to run in the browser, we'll use GopherJS. GopherJS is a compiler that compiles Go into JavaScript, which can then run in browsers. We'll use two Go packages:
go/format
package of the Go standard library. It exposes all of the functionality we need to havegofmt
behavior.github.com/gopherjs/gopherjs/js
package. If you've ever used cgo and didimport "C"
to interface with the C world, it's very much like that, except the semantics are more like thereflect
package. In the end, it lets you access and modify things in the JavaScript world.
Building JavaScript that implements gofmt
So, let's get started. I am assuming you already have the current version of Go installed. First, you'll need to install the GopherJS compiler if you don't already have it. Its README has a detailed Installation and Usage section, but it boils down to running one command:
$ go get -u github.com/gopherjs/gopherjs
If you have your $GOPATH/bin
added to your PATH
, you'll be able to easily invoke the installed gopherjs
binary.
Now, let's create a small Go package that'll implement our functionality. Make a new directory somewhere in your GOPATH
, cd
into it and write a main.go
:
$ mkdir -p $(go env GOPATH)/src/github.com/you/gofmt
$ cd $(go env GOPATH)/src/github.com/you/gofmt
$ touch main.go
package main
import ("go/format"; "github.com/gopherjs/gopherjs/js")
func main() {
js.Global.Set("gofmt", func(code string) string {
gofmted, _ := format.Source([]byte(code))
return string(gofmted)
})
}
Then run gopherjs build
in same directory to build it:
$ gopherjs build --minify
The --minify
flag causes it to write minified output. (You'd also better use HTTP compression ala Content-Encoding: gzip
when serving it, because the generated JavaScript compresses very well.)
The output will be in the form of a gofmt.js
file (and an optional source map in gofmt.js.map
, useful during development). We can then include it in any HTML page:
<script src="gofmt.js" type="text/javascript"></script>
Once executed, a gofmt
function will be available for the rest of your JavaScript code to use. The function will format any Go code you provide as a string, returning the output as a string:
That's 10 lines which implements gofmt
—we're done! Okay, not quite. I'm ignoring errors for now, and the code itself is not gofmt
ed. We'll come back and fix that next, but first, let's see what's happening in the code.
Code Explanation
First, we're creating a function literal that takes a string
as input and returns a string
. format.Source
is used to format the incoming Go source code, and then it's returned. We're reusing a lot of existing Go code, the same that the real gofmt
command uses, which is well tested and maintained.
Then, we're using js.Global.Set
with the parameters "gofmt" and our function literal.
js.Global
is documented as:
Global gives JavaScript's global object ("window" for browsers and "GLOBAL" for Node.js).
And Set
is:
Set assigns the value to the object's property with the given key.
What we're doing is assigning the JavaScript variable named "gofmt" with the value of a func we've created.
GopherJS performs the neccessary conversions between Go types and JavaScript types, as described in the table at the top of https://godoc.org/github.com/gopherjs/gopherjs/js. Specifically:
Go type | JavaScript type | Conversions back to interface{} |
---|---|---|
string | String | string |
functions | Function | func(...interface{}) *js.Object |
So Go's string
becomes a JavaScript String
type, and a Go func
becomes JavaScript Function
.
By making that js.Global.Set("gofmt", func (code string) string { ... })
call, what we've done can be effectively expressed with the following JavaScript code:
window.gofmt = function(code) {
// Implementation of gofmt, in JavaScript, done for you by the GopherJS compiler.
// Lots and lots of generated JavaScript code here...
return gofmted;
}
Just like that, you've got an implementation of gofmt
that can run in the browser.
Finishing Touches
Let's come back to the code and make it nicer. We'll gofmt
it, to avoid the irony, and add some error checking:
package main
import (
"go/format"
"github.com/gopherjs/gopherjs/js"
)
func main() {
js.Global.Set("gofmt", func(code string) string {
gofmted, err := format.Source([]byte(code))
if err != nil {
return err.Error()
}
return string(gofmted)
})
}
Now we get nicer output when there's an error formatting the code:
But maybe you'd rather just fall back to showing the original input if formatting fails:
gofmted, err := format.Source([]byte(code))
if err != nil {
return code
}
Or maybe you want to include the error message on top:
gofmted, err := format.Source([]byte(code))
if err != nil {
return err.Error() + "\n\n" + code
}
Or maybe return a boolean indicating success:
func(code string) (string, bool) {
gofmted, err := format.Source([]byte(code))
if err != nil {
return code, false
}
return string(gofmted), true
}
This depends on the exact needs of your JavaScript application. I'll leave it to you.
Additional Notes
One of potential concerns might be that the generated JavaScript is relatively large, so it may negatively impact page loading time. There are tradeoffs in compiling Go to JavaScript, and large size is one of them. I have two thoughts on that: 1. You can always do progressive enhancement. Load your page as usual, but use [`async`](https://www.w3schools.com/tags/att_script_async.asp) attribute in the script tag for the gofmt code. Page loads as quickly as before, but during the first second, user can't gofmt (it can print a nice placeholder message like "hang on, that functionality is still loading...", or maybe fall back to a remote server). Once the gofmt functionality loads, it starts working locally. Most people are unlikely to notice that first second the app isn't completely functional yet. And on second load, it'll be cached, etc. 2. In the future, WebAssembly will let you do/write things that will beat pure JavaScript in terms of parse/load speed. The Go code you write today will work largely unmodified, but become more optimized in the future. The large generated JavaScript output is a short term implementation detail. In the end, there are tradeoffs between various approaches, so you should use the tools and approaches that make most sense for your project.Conclusion
Hopefully, you saw just how easy it is to take some non-trivial functionality that happens to be already written in Go, and expose it to your JavaScript application.
Next week, I'll post another example of using net/http
, encoding/csv
packages in the browser to stream CSV data from a server and parse it on the fly, so stay tuned.
Leave a reaction at the bottom of this post if you found it helpful, and comment if you have feedback.
As a result of this blog post, within hours, at least two tools have been updated to
gofmt
their output:That's great to see!