Skip to content

JavaScript Tips and Gotchas

Jonathan Hall edited this page Aug 4, 2023 · 24 revisions

Gotchas

Some gotchas to not forget when interacting with JavaScript:

  • Callbacks from external JavaScript code into Go code can never be blocking.

  • Do not use items or fields of type js.Object directly. *js.Object must always be a pointer. A struct that embeds a *js.Object must always be a pointer.

    type t struct {
    	*js.Object // so far so good
    	i int `js:"i"`
    }
    
    var v = t{}   // wrong: not a pointer
    var v2 = &t{} // right
  • When creating a struct literal with a *js.Object in it, you have to initialize any other fields separately. Try it.

    type T struct {
    	*js.Object
    	I int `js:"i"`
    }
    
    // You can't initialize I in the struct literal.
    // This doesn't work.
    t1 := &T{Object: js.Global.Get("Object").New(), I: 10}
    fmt.Printf("%#v\n", t1.I) // prints 0
    
    // You have to do this
    t2 := &T{Object: js.Global.Get("Object").New()}
    t2.I = 10
    fmt.Printf("%#v\n", t2.I) // prints 10
  • In structs with *js.Object and js-tagged fields, if the *js.Object is in the struct directly, it must be the first field. If it's in the struct indirectly (in an embedded struct, however deep), then a) it must be the first field in the struct it's in, and b) the struct it's in must be the first field in the outer struct. Because when GopherJS looks for the *js.Object field, it only looks at the field at index 0.

    // This doesn't work
    type s1 struct {
    	i int `js:"i"`
    	*js.Object // Must be first
    }
    
    // This doesn't work
    type s2 struct {
    	*s1 // the *js.Object in s1 is not first
    	i2 int `js:"i2"`
    }
    
    // This definitely doesn't work
    type s3 struct {
    	i2 int `js:"i2"`
    	*s1 // s1 isn't first, AND *js.Object in s1 isn't first
    }
  • In structs with *js.Object fields, the object exists in two parts. The inner *js.Object part stores the fields JavaScript will see. Those fields must be tagged with js:"fieldName". Other untagged fields will be stored in the outer object. Try it.

    type T struct {
    	*js.Object
    	I int `js:"i"`
    	J int
    }
    
    t1 := &T{Object: js.Global.Get("Object").New(), J: 20}
    t1.I = 10
    fmt.Printf("%#v\n", t1) // &main.T{Object:(*js.Object)(0x1), I:10, J:20}
    println(t1) // Console shows something similar to
                // { I: 0, J: 20, Object: { i: 10 } }
  • Putting "non-scalar" types (like slices, arrays, and maps) in JS-structs doesn't work. See https://github.com/gopherjs/gopherjs/issues/460.

  • Decoding JSON

    • Decoding JSON into pure-Go data structures (where no struct has *js.Object fields or js:"..." tags) using encoding/json works.
    • Decoding JSON into pure-JS data structures (where every struct has *js.Object fields and every field has js:"..." tags) using the native JSON parser works.
    • Decoding JSON into mixed Go/JS data structures (where every struct has a *js.Object field but not all fields have js:"..." tags) is problematic. encoding/json does not initialize the *js.Object field, and if you try to write UnmarshalJSON methods for your types to do so, you have trouble reading embedded structs.
  • Goroutines not waking up from sleep when you expect. (E.g. in a small app that creates a new div with the current time in it every minute, I've seen it be late by several seconds.) In Chrome, at least, if a tab loses focus, Chrome will delay timers firing to no more than once per second. (It may have to be completely hidden or off screen, not just lose focus, for this to happen. In the example above, I was in a completely different workspace.) Timers are how GopherJS implements goroutine scheduling, including time.Sleep. Web workers do not have this, uh, feature. There's a small Javascript shim to replace regular timers with web worker timers, and it works with GopherJS. See here and here. Just load HackTimer (from the 2nd link) before your GopherJS code (and possibly before any other JS code, if appropriate) and you're good. (Even with this shim, timers still aren't completely reliable, but they're a lot closer than before.)

Some gotchas when writing Go for transpilation:

  • The fmt library is very large, so excluding it from your codebase can drastically reduce JS size.
    • There is a package in-progress for the purposes of avoiding fmt: fmtless
    • Beware of dependencies that import fmt; in Go 1.6, this includes encoding/json (and this won't change)
    • Don't use fmt.Errorf, use errors.New.
    • Don't use fmt.Sprintf, use strconv.
    • Don't use fmt.Println, use println (which gopherjs converts to console.log)
  • For making HTTP requests, don't use net/http, as importing this will also compile-in the entire TLS stack! Use GopherJS bindings to XmlHTTPRequest instead. For code that'll be compiled either to Go or JS, use a function that's constrained by build tags, so in JS it uses XHR and in Go it uses net/http.
  • The built-in encoding/json library can add a lot of bloat. If you only need to unmarshal JSON, use github.com/cathalgarvey/fmtless/encoding/json, which can save a lot of space.

Tips

Some tips for interacting with JavaScript:

  • You can "wrap" JavaScript objects in Go structs to access their fields more natively. Try it.

    // Simulate an object you get from JavaScript, like an event
    eJs := js.Global.Get("Object").New()
    eJs.Set("eventSource", "Window")
    println(eJs) // {eventSource: "Window"}
    
    // Create a type to access the event slots via go
    type Event struct {
    	*js.Object
    	EventSource string `js:"eventSource"`
    }
    
    // "Wrap" the JS object in a Go struct, access its fields directly
    eGo := &Event{Object: eJs}
    fmt.Printf("%#v\n", eGo.EventSource) // "Window"
  • When making a call via (*js.Object).Call to non-GopherJS code, a thrown error can be caught by using defer and recover. Try it.

    func parse(s string) (res *js.Object, err error) {
    	defer func() {
    		e := recover()
    
    		if e == nil {
    			return
    		}
    
    		if e, ok := e.(*js.Error); ok {
    			err = e
    		} else {
    			panic(e)
    		}
    	}()
    
    	res = js.Global.Get("JSON").Call("parse", s)
    
    	return res, err
    }

Some tips for shrinking the compiled output

  • Use the --minify flag in the compiler to shrink the produced JavaScript as much as possible
  • Further shrinking be achieved by using GZIP compression. Modern web browsers allow for GZIP compressed JavaScript to be sent instead of JavaScript. This can be done by GZIP compressing the GopherJS output and then serving it with a wrapper around the http.ServeContent function:
// compress the GopherJS output with the command: gzip -9 -k -f frontend/js/myproject.js
// creating the file myproject.js.gz


//servePreCompress serves the GZIP version when the web browsers asks for the normal version
func servePreCompress(path, dir string) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
			w.WriteHeader(http.StatusNotImplemented)
			fmt.Fprintln(w, "Only send via gzip")
			return
		}

		content, err := os.Open(dir + path )
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprintln(w, "cannot open file: "+err.Error())
			return
		}
		w.Header().Set("Content-Encoding", "gzip")
		http.ServeContent(w, r, path, time.Now(), content)
	}
}

...
//register the handler to catch the request for the normal js file
//This request it trigger by the foillowing html:
//     <script src="./js/myproject.js"></script> 
http.HandleFunc("/js/myproject.js", servePreCompress("/js/myproject.js.gz", "/var/www"))