Extending GopherJS

Feb 3, 2018 at 1:00PM
Caleb Doxsey

On January 30th, 2018 I gave a talk at the Boston Go meetup entitled "Extending GopherJS". This post is a summary of the contents of that talk. All the example code can be found here: github.com/calebdoxsey/tutorials

What is GopherJS

GopherJS is a compiler for Go that compiles Go code into Javascript intended to run in the browser. You can use it in a similar way to the go command:

gopherjs build -o example.js

You can also use the serve command locally:

gopherjs serve

And visit localhost:8080.

Programs are standard Go programs:

package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

Example 1

Although it may appear to be simply a blank page, if you open your developer console you can see that it printed "Hello World" to the console.

Javascript

Javascript can be accessed via the gopherjs/js library:

package main

import "github.com/gopherjs/gopherjs/js"

func main() {
    js.Global.Get("document").Call("write", "Hello World")
}

Example 2

This API is sufficient to call any function available in Javascript and bindings exist for popular libraries like React:

//+build ignore

package main

func (r HelloMessageDef) Render() react.Element {
    return react.Div(nil,
        react.S("Hello "+r.Props().Name),
    )
}

Compatibility

GopherJS is surprisingly complete. It implements the entire language and most packages. To demonstrate this I put together a concurrent merge sort algorithm:

package main


// ConcurrentMergeSort sorts a list concurrently
func ConcurrentMergeSort(xs []int) []int {
    switch len(xs) {
    case 0:
        return nil
    case 1, 2:
        return merge(xs[:1], xs[1:])
    default:
        lc, rc := make(chan []int), make(chan []int)
        go func() {
            lc <- ConcurrentMergeSort(xs[:len(xs)/2])
        }()
        go func() {
            rc <- ConcurrentMergeSort(xs[len(xs)/2:])
        }()
        return merge(<-lc, <-rc)
    }
}

And the definition of merge:

package main


func merge(l, r []int) []int {
    m := make([]int, 0, len(l)+len(r))
    for len(l) > 0 || len(r) > 0 {
        switch {
        case len(l) == 0:
            m = append(m, r[0])
            r = r[1:]
        case len(r) == 0:
            m = append(m, l[0])
            l = l[1:]
        case l[0] <= r[0]:
            m = append(m, l[0])
            l = l[1:]
        case l[0] > r[0]:
            m = append(m, r[0])
            r = r[1:]
        }
    }
    return m
}

This algorithm recursively merges a list of integers by starting a goroutine on each half of the list, sorting that half, then merging the results back together again. In practice the overhead for all the channels and goroutines means this approach is probably not one you would use in the real world (though if you can avoid creating quite so many goroutines, the concurrency might be worthwhile), but it does function as a good stress test for the scheduler.

The main program is as follows:

package main

import (
    "bytes"
    "fmt"
    "math/rand"
    "time"

    "github.com/gopherjs/gopherjs/js"
)

func main() {
    start := time.Now()
    var xs []int
    for i := 0; i < 10000; i++ {
        xs = append(xs, rand.Intn(1000000))
    }
    xs = ConcurrentMergeSort(xs)
    end := time.Now()

    var buf bytes.Buffer
    fmt.Fprintf(&buf, "sorted %d elements in %s\n", len(xs), end.Sub(start))
    for _, x := range xs {
        fmt.Fprintf(&buf, "%8d\n", x)
    }
    js.Global.Get("document").Call("write", buf.String())
}

You can run it here: Example 3

So sorting 10,000 integers using a whole lot of goroutines is no big deal for GopherJS. This is a rather stunning technical achievement given that Javascript doesn't actually have any threading capabilities (besides web-workers, which aren't suitable for this task, because they can't share memory)

What Doesn't Work

However not everything works in GopherJS. Notably:

If you try to run programs that uses these features you will get an error. Consider this program:

package main

import "fmt"


func main() {
    fmt.Print("enter your name: ")
    var name string
    fmt.Scanln(&name)
    fmt.Println("\n\nyou entered:", name)
}

Example 4.

You will see:

Sandbox

GopherJS can't provide these capabilities, because your Browser can't provide these capabilities. It won't let you write files, or bind a port, or make a raw TCP connection, and it definitely won't let you execute an arbitrary program, for obvious reasons.

But these kinds of programs are Go's bread and butter. Go is more than just a programming language. It's also a set of powerful, well thought-out API abstractions for working with the operating system.

In one sense, Go is the opposite of Haskell, a language where the focus is on purity and avoiding side effects. But side effects are why Go exists. It's a practical language focused on developer productivity and programs that work with files or network sockets are precisely the kinds of programs you want to write in Go.

Nearly all Go programs you run into in the wild need some access to the Operating System, which means GopherJS can't actually run very many Go programs.

So is GopherJS just a brilliant technical curiosity?

The Most Useless Machine in the World

Emulation

What if we emulated an operating system's API? If the emulation layer is close enough to the real thing, existing programs which use the operating system APIs will never be the wiser.

There's actually a pretty profound idea behind this too. In some sense, all programming is an act of deception. The very words you're reading on your screen right now are drawn with miniscule pixels using programming constructs which have no notion of language or understanding of the alphabet. They're merely a tool the programmer uses to achieve the goal of letterlike images appearing on a display.

(as an aside, if you're interested in this idea, consider Searle's Chinese room argument)

So since we can't break out of the sandbox into the outside world, we'll create a virtual world within the sandbox. The goal is to create an environment where unmodified Go programs can run. (that is ones which use features real Go programs use)

System Calls

So how does Go integrate with the operating system anyway?

It's a testament to the utility of the language's APIs that most Go programmers never really have to concern themselves with the answer to this question. They can build fairly sophisticated Go programs without ever really talking to the operating system directly, instead relying on the os or net packages, which abstract most of these details away.

Nevertheless, the answer is fairly straightforward. Go interfaces with the operating system using system calls. Different operating systems and machine architectures have different mechanisms for invoking system calls. For example here is how a system call is implemented for linux in amd64:

// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT	·Syscall(SB),NOSPLIT,$0-56
	CALL	runtime·entersyscall(SB)
	MOVQ	a1+8(FP), DI
	MOVQ	a2+16(FP), SI
	MOVQ	a3+24(FP), DX
	MOVQ	$0, R10
	MOVQ	$0, R8
	MOVQ	$0, R9
	MOVQ	trap+0(FP), AX	// syscall entry
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001
	JLS	ok
	MOVQ	$-1, r1+32(FP)
	MOVQ	$0, r2+40(FP)
	NEGQ	AX
	MOVQ	AX, err+48(FP)
	CALL	runtime·exitsyscall(SB)
	RET
ok:
	MOVQ	AX, r1+32(FP)
	MOVQ	DX, r2+40(FP)
	MOVQ	$0, err+48(FP)
	CALL	runtime·exitsyscall(SB)
	RET

Linux is a large, monolithic kernel with lots and lots of system calls: System Call Listing. As an example of how these are implemented in Go, consider the read System Call. It's signature is documented in C:

ssize_t read(int fd, void *buf, size_t count);

The definition can be found here: https://golang.org/src/syscall/zsyscall_linux_amd64.go.

func read(fd int, p []byte) (n int, err error) {
  var _p0 unsafe.Pointer
  if len(p) > 0 {
    _p0 = unsafe.Pointer(&p[0])
  } else {
    _p0 = unsafe.Pointer(&_zero)
  }
  r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
  n = int(r0)
  if e1 != 0 {
    err = errnoErr(e1)
  }
  return
}

There's a whole lot of unsafe pointers and arrays when working with system calls, but hopefully the basic mechanism is clear. Most programs only use a few system calls, so we don't have to emulate all of them.

How GopherJS Works

GopherJS works by modifying the standard package code included with Go so that it can run in the browser. The pseudo file system is in a folder called natives in the GopherJS project code: natives.

Any code not altered by this file system will be preserved, so the goal is to make the minimal number of changes to get things to work. Consider syscall_unix.go:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) {
  if f := syscall("Syscall"); f != nil {
    r := f.Invoke(trap, a1, a2, a3)
    return uintptr(r.Index(0).Int()), uintptr(r.Index(1).Int()), Errno(r.Index(2).Int())
  }
  if trap == SYS_WRITE && (a1 == 1 || a1 == 2) {
    array := js.InternalObject(a2)
    slice := make([]byte, array.Length())
    js.InternalObject(slice).Set("$array", array)
    printToConsole(slice)
    return uintptr(array.Length()), 0, 0
  }
  if trap == SYS_EXIT {
    runtime.Goexit()
  }
  printWarning()
  return uintptr(minusOne), 0, EACCES
}

This bypasses the assembly function we saw above, instead calling this bit of Go code, which allows the use of a native syscall module in Node, handles writing to stdout/stderr, the special exit system call, but otherwise shows a warning on any other system call.

I modified GopherJS slightly to make it easier to change these system calls: My Changes. This introduces a RegisterSyscallHandler function in a subpackage called ext. This allows us to easily customize what happens when these system calls are used.

An Example: stdout to Page

Let's see it in use. Instead of printing data to the console, let's print it to the page using document.write:

//+build js

package main

import (
    "syscall"

    "github.com/gopherjs/gopherjs/ext"
    "github.com/gopherjs/gopherjs/js"
)


var minusOne = -1

func init() {
    ext.RegisterSyscallHandler(syscall.SYS_WRITE, func(fd, buf, count uintptr) (r1, r2 uintptr, err syscall.Errno) {
        switch fd {
        case uintptr(syscall.Stdout), uintptr(syscall.Stderr):
            js.Global.Get("document").Call("write", "<pre>"+uint8ArrayToString(buf)+"</pre>")
            return count, 0, 0
        }
        return uintptr(minusOne), 0, syscall.EACCES
    })
}

When the write system call is invoked, we look at the first argument, which is the file descriptor:

ssize_t write(int fd, const void *buf, size_t count);

We check if the file descriptor is stdout or stderr, and if it is, we convert the raw uintptr buffer into a string, and call document.write with it.

package main

import (
    "fmt"
    "time"

    "github.com/gopherjs/gopherjs/js"
)


func main() {
    for range time.Tick(time.Second) {
        fmt.Println("Hello World")
    }
}


func uint8ArrayToString(buf uintptr) string {
    array := js.InternalObject(buf)
    slice := make([]byte, array.Length())
    js.InternalObject(slice).Set("$array", array)
    return string(slice)
}

With the registered system call handler, we can now call fmt.Println like we did before, except this time the behavior is different: Example 5

A Virtual File System

Let's build something more interesting: A Virtual File System.

First use an init function to register the virtual file system syscall handlers:

package main

import (
    "syscall"

    "github.com/gopherjs/gopherjs/ext"
)


var vfs interface {
    Close(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
    Open(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
    Read(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
    Write(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
} = newVirtualFileSystem()

func init() {
    ext.RegisterSyscallHandler(syscall.SYS_CLOSE, vfs.Close)
    ext.RegisterSyscallHandler(syscall.SYS_OPEN, vfs.Open)
    ext.RegisterSyscallHandler(syscall.SYS_READ, vfs.Read)
    ext.RegisterSyscallHandler(syscall.SYS_WRITE, vfs.Write)

    // ignore fcntl
    ext.RegisterSyscallHandler(syscall.SYS_FCNTL, func(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
        return 0, 0, 0
    })
}

Here are the types we will use:

package main

var minusOne = -1

// Our virtual file system contains files and references to files
// A file is just a slice of bytes
// A reference also tracks the position within the file


type (
    virtualFile struct {
        data []byte
    }
    virtualFileReference struct {
        file *virtualFile
        pos  int
    }
    virtualFileSystem struct {
        files  map[string]*virtualFile
        fds    map[uintptr]*virtualFileReference
        nextFD uintptr
    }
)

func newVirtualFileSystem() *virtualFileSystem {
    return &virtualFileSystem{
        files:  make(map[string]*virtualFile),
        fds:    make(map[uintptr]*virtualFileReference),
        nextFD: 1000,
    }
}

And here's how we implement the Write function:

package main

import (
    "syscall"

    "github.com/gopherjs/gopherjs/js"
)


// Write a file: http://man7.org/linux/man-pages/man2/write.2.html
//
//       ssize_t write(int fd, const void *buf, size_t count);
//
func (vfs *virtualFileSystem) Write(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
    fd := a1
    buf := uint8ArrayToBytes(a2)
    cnt := a3

    // find our file descriptor
    ref, ok := vfs.fds[fd]
    if !ok {
        return uintptr(minusOne), 0, syscall.EBADF
    }

    // append to the file data and move the cursor
    ref.file.data = append(ref.file.data[ref.pos:], buf...)
    ref.pos += len(buf)

    return cnt, 0, 0
}

The other functions are very similar. Let's use our new file system:

package main

import (
    "io"
    "io/ioutil"
    "os"
)


func main() {
    // notice how we are able to seamlessly use higher-level libraries
    err := ioutil.WriteFile("/tmp/hello.txt", []byte("Example 06\n"), 0777)
    if err != nil {
        panic(err)
    }

    f, err := os.Open("/tmp/hello.txt")
    if err != nil {
        panic(err)
    }
    io.Copy(os.Stdout, f)
    f.Close()
}

This example writes to a file and then reads from it again. I used different reading/writing mechanisms to demonstrate that once you've implemented the lowest layer, all the code above it will work. You can run it here: Example 6

A Virtual File System: Persistence

Of course this file system is purely in memory so disappears each time we refresh the page. We can use IndexedDB to implement persistence. First update the virtual file system:

package main

import (
    "errors"

    "github.com/gopherjs/gopherjs/js"
)

var minusOne = -1

// Our virtual file system contains files and references to files
// A file is just a slice of bytes
// A reference also tracks the position within the file

type (
    virtualFileReference struct {
        changed bool
        path    string
        data    []byte
        pos     int
    }
    virtualFileSystem struct {
        db     *js.Object
        fds    map[uintptr]*virtualFileReference
        nextFD uintptr
    }
)

func newVirtualFileSystem() *virtualFileSystem {
    type Result struct {
        vfs *virtualFileSystem
        err error
    }
    c := make(chan Result, 1)
    req := js.Global.Get("indexedDB").Call("open", "vfs")
    req.Set("onupgradeneeded", func(evt *js.Object) {
        db := evt.Get("target").Get("result")
        db.Call("createObjectStore", "files")
    })
    req.Set("onsuccess", func(evt *js.Object) {
        c <- Result{vfs: &virtualFileSystem{
            db:     req.Get("result"),
            fds:    make(map[uintptr]*virtualFileReference),
            nextFD: 1000,
        }}
    })
    res := <-c
    if res.err != nil {
        panic(res.err)
    }
    return res.vfs
}

Open reads the file from IndexedDB:

package main

import (
    "errors"
    "os"
    "syscall"

    "github.com/gopherjs/gopherjs/js"
)

// Open a file: http://man7.org/linux/man-pages/man2/open.2.html
//
//        int open(const char *pathname, int flags, mode_t mode);
//
func (vfs *virtualFileSystem) Open(a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) {
    pathname := uint8ArrayToString(a1)
    flags := int(a2)
    mode := os.FileMode(a3)


    // See if the file exists, if it doesn't, and we passed O_CREATE, create it


    type Result struct {
        res *js.Object
        err error
    }
    c := make(chan Result, 1)
    tx := vfs.db.Call("transaction", js.S{"files"}, "readonly")
    req := tx.Call("objectStore", "files").Call("get", pathname)
    req.Set("onsuccess", func(evt *js.Object) {
        c <- Result{
            res: evt.Get("target").Get("result"),
        }
    })
    req.Set("onerror", func(evt *js.Object) {
        c <- Result{
            err: errors.New(evt.Get("target").Get("error").String()),
        }
    })
    res := <-c
    if res.err != nil {
        return 0, 0, syscall.EACCES
    }


    ref := &virtualFileReference{
        path: pathname,
    }
    if bs, ok := res.res.Interface().([]byte); ok && bs != nil {
        ref.data = bs
    } else {
        if flags&os.O_CREATE == 0 {
            return 0, 0, syscall.ENOENT
        }
    }

    // Truncate it if we passed O_TRUNC
    if flags&os.O_TRUNC != 0 {
        ref.data = nil
    }

    // Generate a file descriptor, and store a reference in the map
    fd := vfs.nextFD
    vfs.nextFD++
    vfs.fds[fd] = ref

    // Return the file descriptor
    return fd, 0, 0
}

Close writes it:

package main

import (
    "errors"
    "syscall"

    "github.com/gopherjs/gopherjs/js"
)

// Close a file: http://man7.org/linux/man-pages/man2/close.2.html
//
//       int close(int fd);
//
func (vfs *virtualFileSystem) Close(a1, a2, a3 uintptr) (r1, r2 uintptr, errno syscall.Errno) {
    fd := a1

    js.Global.Get("console").Call("log", "::CLOSE", fd)

    // See if the file descriptor exists. If it doesn't, return an error
    ref, ok := vfs.fds[fd]
    if !ok {
        return uintptr(minusOne), 0, syscall.EBADF
    }


    // flush the data to the db
    if ref.changed {
        type Result struct {
            err error
        }
        c := make(chan Result, 1)
        tx := vfs.db.Call("transaction", js.S{"files"}, "readwrite")
        req := tx.Call("objectStore", "files").Call("put", ref.data, ref.path)
        req.Set("onsuccess", func(evt *js.Object) {
            c <- Result{}
        })
        req.Set("onerror", func(evt *js.Object) {
            c <- Result{
                err: errors.New(evt.Get("target").Get("error").String()),
            }
        })
        res := (<-c)
        if res.err != nil {
            return 0, 0, syscall.EACCES
        }
    }


    // Close the file descriptor by removing it
    delete(vfs.fds, fd)

    return 0, 0, 0
}

Here's an example:

package main

import (
    "io/ioutil"
    "os"

    "github.com/gopherjs/gopherjs/js"
)

func main() {
    js.Global.Get("document").Get("documentElement").Set("innerHTML", `<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <form onsubmit="javascript:event.preventDefault()">
            <input type="text" id="filename" placeholder="/tmp/example.txt">
            <input type="submit" value="Load" onclick="onloadfile()">
            <br>
            <br>
            <textarea id="filecontents" cols="70" rows="5" placeholder="contents"></textarea>
            <br>
            <input type="submit" value="Save" onclick="onsavefile()">
        </form>
    </body>
</html>
    `)


    js.Global.Set("onloadfile", func() {
        filename := js.Global.Get("document").Call("getElementById", "filename").Get("value").String()
        go func() {
            bs, err := ioutil.ReadFile(filename)
            if err != os.ErrNotExist && err != nil {
                panic(err)
            }
            js.Global.Get("document").Call("getElementById", "filecontents").Set("value", string(bs))
        }()
    })
    js.Global.Set("onsavefile", func() {
        filename := js.Global.Get("document").Call("getElementById", "filename").Get("value").String()
        filecontents := js.Global.Get("document").Call("getElementById", "filecontents").Get("value").String()
        go func() {
            err := ioutil.WriteFile(filename, []byte(filecontents), 0777)
            if err != nil {
                panic(err)
            }
        }()
    })

}

Example 7

Network Sockets

The same approach could be implemented for network sockets. Implement syscall handlers for socket, bind, listen, connect, etc...

Rather than show you the nitty, gritty details, I'll show you a fully fledged example.

Our goal will be to run a net/http server in GopherJS.

Network Server

The "server" looks like this: (remember this http server runs in the browser)

package main

import (
    "io"
    "net/http"

    "github.com/hashicorp/yamux"

    "github.com/gopherjs/gopherjs/js"
)


func init() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        io.WriteString(w, "Hello World!")
    })
}

func main() {
    ws := js.Global.Get("WebSocket").New("ws://localhost:5000/listen/5001")

    conn := newWSConn(ws)
    defer conn.Close()

    li, err := yamux.Server(conn, yamux.DefaultConfig())
    if err != nil {
        panic(err)
    }
    defer li.Close()

    err = http.Serve(li, nil)
    if err != nil {
        panic(err)
    }
}


func traceWS(ws *js.Object) {
    ws.Call("addEventListener", "open", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "open", evt)
    })
    ws.Call("addEventListener", "message", func(evt *js.Object) {
        enc := js.Global.Get("TextDecoder").New()
        msg := enc.Call("decode", evt.Get("data"))

        js.Global.Get("console").Call("log", "message", msg)
    })
    ws.Call("addEventListener", "error", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "error", evt)
    })
    ws.Call("addEventListener", "close", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "close", evt)
    })
}

Converting a websocket into a net.Conn:

package main

import (
    "log"
    "net"
    "time"

    "github.com/gopherjs/gopherjs/js"
)


type wsconn struct {
    ws  *js.Object
    rdr *ChannelReader
}

var _ net.Conn = (*wsconn)(nil)

func newWSConn(ws *js.Object) *wsconn {
    ws.Set("binaryType", "arraybuffer")
    out := make(chan []byte, 1)
    ws.Call("addEventListener", "message", func(evt *js.Object) {
        out <- toBytes(evt.Get("data"))
    })
    rdr := NewChannelReader(out)
    return &wsconn{
        ws:  ws,
        rdr: rdr,
    }
}


func (c *wsconn) Read(b []byte) (n int, err error) {
    n, err = c.rdr.Read(b)
    return n, err
}

func (c *wsconn) Write(b []byte) (n int, err error) {
    buf := js.NewArrayBuffer(b)
    c.ws.Call("send", buf)
    return len(b), nil
}

func (c *wsconn) Close() error {
    c.ws.Call("close")
    return nil
}

func (c *wsconn) LocalAddr() net.Addr {
    return websocketAddress{c.ws.Get("url").String()}
}

func (c *wsconn) RemoteAddr() net.Addr {
    return websocketAddress{c.ws.Get("url").String()}
}

func (c *wsconn) SetDeadline(t time.Time) error {
    c.SetReadDeadline(t)
    c.SetWriteDeadline(t)
    return nil
}

func (c *wsconn) SetReadDeadline(t time.Time) error {
    c.rdr.SetDeadline(t)
    return nil
}

func (c *wsconn) SetWriteDeadline(t time.Time) error {
    log.Println("SetWriteDeadline not implemented")
    return nil
}

func toBytes(obj *js.Object) []byte {
    return js.Global.Get("Uint8Array").New(obj).Interface().([]byte)
}

type websocketAddress struct {
    url string
}

func (wsa websocketAddress) Network() string {
    return "ws"
}

func (wsa websocketAddress) String() string {
    return wsa.url
}

The proxy's listener:

package main

import (
    "log"
    "net"
    "net/http"
    "strings"
    "time"

    "github.com/hashicorp/yamux"
)

func handleListen(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "*")

    // a path like: /listen/1234
    port := r.URL.Path[strings.LastIndexByte(r.URL.Path, '/')+1:]


    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    defer ws.Close()

    wsc := &binaryWSConn{Conn: ws}

    dst, err := yamux.Client(wsc, yamux.DefaultConfig())
    if err != nil {
        return
    }
    defer dst.Close()

    src, err := net.Listen("tcp", "127.0.0.1:"+port)
    if err != nil {
        return
    }
    defer src.Close()


    // if the "server" disconnects, close the listener too
    go func() {
        for range time.Tick(time.Second) {
            if dst.IsClosed() {
                src.Close()
                return
            }
        }
    }()

    log.Println("started listener", src.Addr())
    defer log.Println("closed listener", src.Addr())

    for {
        srcc, err := src.Accept()
        if err != nil {
            log.Println("error accepting connection:", err)
            break
        }

        dstc, err := dst.Open()
        if err != nil {
            srcc.Close()
            log.Println("error opening connection:", err)
            break
        }

        go func() {
            defer srcc.Close()
            defer dstc.Close()
            err := proxy(dstc, srcc)
            if err != nil {
                log.Println("error handling connection:", err)
            }
        }()
    }
}

The proxy's main code:

package main

import (
    "io"
    "log"
    "net/http"
    "os"

    "github.com/gorilla/websocket"
    "golang.org/x/sync/errgroup"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin:     func(r *http.Request) bool { return true },
}

func main() {
    log.SetFlags(0)

    http.HandleFunc("/dial/", handleDial)
    http.HandleFunc("/listen/", handleListen)

    addr := os.Getenv("ADDR")
    if addr == "" {
        addr = "127.0.0.1:5000"
    }
    log.Printf("starting http server addr=%s\n", addr)
    http.ListenAndServe(addr, nil)
}


func proxy(dst, src io.ReadWriter) error {
    var eg errgroup.Group
    eg.Go(func() error {
        _, err := io.Copy(dst, src)
        return err
    })
    eg.Go(func() error {
        _, err := io.Copy(src, dst)
        return err
    })
    return eg.Wait()
}

Incidentally I love that proxy function. So simple yet it works.

To run this example first start the proxy in a terminal:

go get github.com/calebdoxsey/tutorials/talks/2018-01-30--extending-gopherjs/example-08/proxy && proxy

You should see: starting http server addr=127.0.0.1:5000.

Now open this link: Example 8: server. In the console you will see: open and a DOM event. You are now running a server in the browser. You can test it by using curl locally:

curl localhost:5001

Or simply browse to localhost:5001 in your browser.

Let's take it one step further. We can run the client via GopherJS too:

package main

import (
    "context"
    "io/ioutil"
    "net"
    "net/http"

    "github.com/gopherjs/gopherjs/js"
)

func main() {
    client := &http.Client{Transport: &http.Transport{
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            host, port, err := net.SplitHostPort(addr)
            ws := js.Global.Get("WebSocket").New("ws://" + host + ":5000/dial/" + port)
            conn := newWSConn(ws)
            return conn, nil
        },
    }}

    resp, err := client.Get("http://127.0.0.1:5001/")

    //...

    bs, _ := ioutil.ReadAll(resp.Body)
    js.Global.Get("document").Call("write", string(bs))
}

func traceWS(ws *js.Object) {
    ws.Call("addEventListener", "open", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "open", evt)
    })
    ws.Call("addEventListener", "message", func(evt *js.Object) {
        enc := js.Global.Get("TextDecoder").New()
        msg := enc.Call("decode", evt.Get("data"))

        js.Global.Get("console").Call("log", "message", msg)
    })
    ws.Call("addEventListener", "error", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "error", evt)
    })
    ws.Call("addEventListener", "close", func(evt *js.Object) {
        js.Global.Get("console").Call("log", "close", evt)
    })
}

Open this link: Example 8: client. Now we're running a browser in a browser! It's browsers all the way down.

I even have a way of running the proxy in GopherJS...

Stop

So... I do have a reason for all this. But that explanation will have to wait for another day.