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 gofmted.

"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 have gofmt behavior.

  • github.com/gopherjs/gopherjs/js package. If you've ever used cgo and did import "C" to interface with the C world, it's very much like that, except the semantics are more like the reflect 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:

Image

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 gofmted. 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:

Image

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 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.

Comments

shurcooL commented 4 months ago · edited

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!

Write Preview Markdown
Jimmy99 commented 4 months ago

Any idea why I get the following errors when I execute gopherjs build --minify

../../../../../../../../usr/local/go/src/runtime/error.go:134:3: undeclared name: throw ../../../../../../../../usr/local/go/src/runtime/error.go:136:12: undeclared name: CallersFrames ../../../../../../../../usr/local/go/src/runtime/error.go:144:3: undeclared name: throw ../../../../../../../../usr/local/go/src/runtime/error.go:148:3: undeclared name: throw ../../../../../../../../usr/local/go/src/runtime/error.go:153:3: undeclared name: throw ../../../../../../../../usr/local/go/src/runtime/error.go:156:3: undeclared name: throw

Write Preview Markdown
shurcooL commented 4 months ago · edited

@Jimmy99, can you file a new issue at https://github.com/gopherjs/gopherjs/issues/new so we can help get to the bottom of that? Please include what version of Go you have installed, and what command you used to install gopherjs (and when that was).

It's most likely because your version of Go isn't 1.8.x. GopherJS requires the current version of Go to build and operate.

Write Preview Markdown
to comment.