Go in the browser

10 October 2016

Dmitri Shuralyov

Software Engineer, Sourcegraph

The Go Programming Language

https://golang.org/ref/spec

Motivation

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 cross-compilation and wide platform support

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 cross-compilation and wide platform support

Go already runs on many platforms.

Go cross-compilation and wide platform support

How about one more?

GopherJS

GopherJS is a compiler that compiles Go to JavaScript, which runs in browsers.

js package for accessing JavaScript world

Package "github.com/gopherjs/gopherjs/js".

js package for accessing JavaScript world

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

js package for accessing JavaScript world

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!")

Cgo is not Go

JS bindings

JS bindings

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")

JS bindings

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.

JS bindings

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.

io.Reader and io.Writer

Implementing io.Reader and io.Writer in browser

Let's try to port command ivy to run in browser.

Implementing io.Reader and io.Writer in browser

CLI commands need a stdin, stdout, stderr to run.

Implementing io.Reader and io.Writer in browser

Easy to implement inside a terminal:

var (
    stdin  io.Reader = os.Stdin
    stdout io.Writer = os.Stdout
    stderr io.Writer = os.Stderr
)

Implementing io.Reader and io.Writer in browser

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>

Implementing io.Reader and io.Writer in browser

Implementing io.Reader and io.Writer in browser

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
}

Implementing io.Reader and io.Writer in browser

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
}

Implementing io.Reader and io.Writer in browser

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
}

Implementing io.Reader and io.Writer in browser

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))
}

ivy in browser

Implementing io.Reader and io.Writer in browser

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)
}

ivy in browser

Streaming HTTP response bodies

Streaming HTTP response bodies

resp, err := http.Get("https://example.com/large.csv")
if err != nil {
    // handle error
}
defer resp.Body.Close()

io.Copy(os.Stdout, resp.Body)

Streaming HTTP response bodies

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);
});

Streaming HTTP response bodies

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)

Cross-platform libraries

Cross-platform libraries

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

Cross-platform libraries

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))
}

Cross-platform libraries

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))
}

Cross-platform libraries

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)
}

Cross-platform libraries

That way, a single codebase can run everywhere.

Challenges

Future

Conclusion

Closing thoughts

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.

Thank you

Dmitri Shuralyov

Software Engineer, Sourcegraph

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)