Go in the browser
10 October 2016
Dmitri Shuralyov
Software Engineer, Sourcegraph
Dmitri Shuralyov
Software Engineer, Sourcegraph
I want to use Go, but it's hard to ignore the browser in 2016.
There are forces pulling you away from Go and its tooling. I decided not to give in.
I wanted to keep:
Go already runs on many platforms.
# Desktop OSes. GOOS=darwin GOARCH=arm64 go build GOOS=linux GOARCH=amd64 go build GOOS=windows GOARCH=arm64 go build GOOS=plan9 GOARCH=amd64 go build # Plan 9. GOOS=linux GOARCH=s390x go build # Linux on IBM z Systems. # Mobile OSes. GOOS=darwin GOARCH=arm64 go build # iOS. GOOS=android GOARCH=arm go build # Android.
Go already runs on many platforms.
5How about one more?
6GopherJS is a compiler that compiles Go to JavaScript, which runs in browsers.
//gopherjs:blocking
.//gopherjs:blocking
.
Package "github.com/gopherjs/gopherjs/js"
.
"C"
pseudo-package to access C world."reflect"
package.
Package "github.com/gopherjs/gopherjs/js"
.
// Object is a container for a native JavaScript object. type Object struct {...} func (*Object) String() string func (*Object) Int() int func (*Object) Float() float64 func (*Object) ... func (*Object) New(args ...interface{}) *Object func (*Object) Get(key string) *Object func (*Object) Set(key string, value interface{}) func (*Object) Call(name string, args ...interface{}) *Object func (*Object) ... // Global is JavaScript's global object ("window" for browsers). var Global *js.Object
Simple JavaScript:
let elem = window.document.getElementById('my-div'); elem.textContent = 'Hello from JavaScript!';
Go equivalent using js
package:
elem := js.Global.Get("window").Get("document").Call("getElementById", "my-div") elem.Set("textContent", "Hello from Go!")
js
package is not pleasant. But only need to touch it "once".honnef.co/go/js/dom
honnef.co/go/js/xhr
net/http
github.com/gopherjs/eventsource
github.com/gopherjs/websocket
github.com/gopherjs/webgl
github.com/go-humble/locstor
honnef.co/go/js/console
github.com/gopherjs/jsbuiltin
github.com/fabioberger/chrome
Package "honnef.co/go/js/dom"
for DOM API.
func (Document) GetElementByID(id string) Element func (*BasicHTMLElement) OffsetHeight() float64 func (*BasicHTMLElement) Focus() func (*BasicNode) SetTextContent(s string) func (*BasicNode) ReplaceChild(newChild, oldChild Node) ... (951 exported symbols)
Previous example using dom
package:
elem := dom.GetWindow().Document().GetElementByID("my-div") elem.SetTextContent("Hello from Go")
Package "github.com/gopherjs/websocket"
for WebSocket API.
// Dial opens a new WebSocket connection. It will block // until the connection is established or fails to connect. func Dial(url string) (net.Conn, error)
Enables use of net/rpc
, net/rpc/jsonrpc
.
Package net/http
(HTTP client only).
resp, err := http.Get("https://example.com/") if err != nil { // handle error } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ...
Implemented via Fetch API. If unavailable, falls back to XMLHttpRequest API.
15
Let's try to port command ivy
to run in browser.
CLI commands need a stdin, stdout, stderr to run.
Easy to implement inside a terminal:
var ( stdin io.Reader = os.Stdin stdout io.Writer = os.Stdout stderr io.Writer = os.Stderr )
Let's use an <pre> and <input> elements in browser.
<html> <head>...</head> <body> <div class="console"> <pre id="output">output goes here</pre> <input id="input" autofocus></input> </div> <!-- ivybrowser.js is built with `gopherjs build`. --> <script src="ivybrowser.js" type="text/javascript"></script> </body> </html>
Writer appends to <pre>'s textContent.
// NewWriter takes a <pre> element and makes an io.Writer out of it. func NewWriter(pre *dom.HTMLPreElement) io.Writer { return &writer{pre: pre} } type writer struct { pre *dom.HTMLPreElement } func (w *writer) Write(p []byte) (n int, err error) { w.pre.SetTextContent(w.pre.TextContent() + string(p)) return len(p), nil }
Reader waits for Enter key, sends <input>'s value.
type reader struct { pending []byte in chan []byte // This channel is never closed, no need to detect it and return io.EOF. } func (r *reader) Read(p []byte) (n int, err error) { if len(r.pending) == 0 { r.pending = <-r.in } n = copy(p, r.pending) r.pending = r.pending[n:] return n, nil }
Reader waits for Enter key, sends <input>'s value.
// NewReader takes an <input> element and makes an io.Reader out of it. func NewReader(input *dom.HTMLInputElement) io.Reader { r := &reader{ in: make(chan []byte, 8), } input.AddEventListener("keydown", false, func(event dom.Event) { ke := event.(*dom.KeyboardEvent) if ke.KeyCode == '\r' { r.in <- []byte(input.Value + "\n") input.Value = "" ke.PreventDefault() } }) return r }
Putting it all together.
io_js.go
// +build js package main import ( "io" "honnef.co/go/js/dom" ) var document = dom.GetWindow().Document() func init() { stdin = NewReader(document.GetElementByID("input").(*dom.HTMLInputElement)) stdout = NewWriter(document.GetElementByID("output").(*dom.HTMLPreElement)) stderr = NewWriter(document.GetElementByID("output").(*dom.HTMLPreElement)) }
We can use io.TeeReader
.
func init() { stdin = NewReader(document.GetElementByID("input").(*dom.HTMLInputElement)) stdout = NewWriter(document.GetElementByID("output").(*dom.HTMLPreElement)) stderr = NewWriter(document.GetElementByID("output").(*dom.HTMLPreElement)) // Send a copy of stdin to stdout (like in most terminals). stdin = io.TeeReader(stdin, stdout) }
resp, err := http.Get("https://example.com/large.csv") if err != nil { // handle error } defer resp.Body.Close() io.Copy(os.Stdout, resp.Body)
var query = 'gopher'; fetch('/large.csv').then(function(response) { var reader = response.body.getReader(); var partialRecord = ''; var decoder = new TextDecoder(); function search() { return reader.read().then(function(result) { partialRecord += decoder.decode(result.value || new Uint8Array, { stream: !result.done }); // query logic ... // Call reader.cancel("No more reading needed."); when result found early. if (result.done) { throw Error("Could not find value after " + query); } return search(); }) } return search(); }).then(function(result) { console.log("Got the result! It's '" + result + "'"); }).catch(function(err) { console.log(err.message); });
func Search(url, query string) (result string, err error) { resp, err := http.Get(url) if err != nil { // handle error } defer resp.Body.Close() r := csv.NewReader(resp.Body) for { record, err := r.Read() if err == io.EOF { return "", fmt.Errorf("could not find value after %q", query) } if err != nil { // handle error } // query logic ... } }
result, err := Search("/large.csv", "gopher") if err != nil { // handle error } fmt.Printf("Got the result! It's %q\n", result)
It's possible to create highly cross-platform libraries with build constraints.
// Package gl is a Go cross-platform binding for OpenGL, with an OpenGL ES 2-like API. // // It supports macOS, Linux and Windows, iOS, Android, modern browsers. package gl
macOS, Linux and Windows via OpenGL 2.1 backend.
// +build 386 amd64 package gl /* ... OpenGL headers */ import "C" func DrawArrays(mode Enum, first, count int) { // https://www.opengl.org/sdk/docs/man2/xhtml/glDrawArrays.xml C.glowDrawArrays(gpDrawArrays, (C.GLenum)(mode), (C.GLint)(first), (C.GLsizei)(count)) }
iOS and Android via OpenGL ES 2.0 backend.
// +build darwin linux // +build arm arm64 package gl /* ... OpenGL ES headers */ import "C" func DrawArrays(mode Enum, first, count int) { // https://www.khronos.org/opengles/sdk/docs/man/xhtml/glDrawArrays.xml C.glDrawArrays(mode.c(), C.GLint(first), C.GLsizei(count)) }
Modern browsers (desktop and mobile) via WebGL 1.0 backend.
// +build js package gl import "github.com/gopherjs/gopherjs/js" // c is the current WebGL context, or nil if there is no current context. var c *js.Object func DrawArrays(mode Enum, first, count int) { // https://www.khronos.org/registry/webgl/specs/1.0/#5.14.11 c.Call("drawArrays", mode, first, count) }
That way, a single codebase can run everywhere.
38.js
output. Viable for single-page apps, not short ad-hoc scripts. Can benefit from improved dead-code eliminaation.gopherjs
build tool, can't use GOARCH=js
go
build
-compiler=gopherjs
.wasm
) is the next step, but not yet ready. It won't be GopherJS.int
, uint16
), compiler errors, refactoring, reusability, blocking code – not callbacks.If you thought Go was fun in the backend, wait until you try it in frontend!
Go made me like frontend programming again. Maybe you'll like it too.
42Dmitri Shuralyov
Software Engineer, Sourcegraph