Concurrency

In go-app, every event and user interaction are handled on a single goroutine. Because some scenarios can have a long execution time, like performing an HTTP request, there is a risk that the UI feels slow or unresponsive.

This document describes how it works and what tools go-app provides to solve this problem.

concurrency.png

UI goroutine

The UI goroutine is the app’s main goroutine. Under the hood, it is an event loop where each event is executed synchronously.

Here are the events that are always executed on the UI goroutine:

Async

func (ctx Context) Async(fn func())

Async() is a Context method that executes a given function on a new goroutine. It is usually used to perform long or blocking operations.

Here is an example where an HTTP request is performed when a page is loaded.

type foo struct {
	app.Compo
}

func (f *foo) OnNav(ctx app.Context) {
	// Launching a new goroutine:
	ctx.Async(func() {
		r, err := http.Get("/bar")
		if err != nil {
			app.Log(err)
			return
		}
		defer r.Body.Close()

		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			app.Log(err)
			return
		}

		app.Logf("request response: %s", b)
	})
}

The difference with manually launching a goroutine is that go-app has no insights about when a manually launched goroutine ceases its execution. It’s not a problem on the client-side but when prerendering on the server-side, go-app has to wait for all launched goroutines to finish their jobs in order to properly generate HTML markup. Therefore, manually launching a goroutine for UI-related purposes introduces reliability issues on the server-side.

Prefer the use of Async() rather than manually launching a goroutine when dealing with UI.

Dispatch

func (ctx Context) Dispatch(fn func())

Dispatch() is a Context method that executes a given function on the UI goroutine. It is used to update the UI after an Async() call, in order to avoid concurrent calls when updating a component field.

Here is an example where an HTTP request is performed when a page is loaded, and its result is stored in a component field:

type foo struct {
	app.Compo

	response []byte
}

func (f *foo) OnNav(ctx app.Context) {
	// Launching a new goroutine:
	ctx.Async(func() {
		r, err := http.Get("/bar")
		if err != nil {
			app.Log(err)
			return
		}
		defer r.Body.Close()

		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			app.Log(err)
			return
		}

		// Storing HTTP response in component field:
		ctx.Dispatch(func() {
			f.response = b
			f.Update()
		})
	})
}

Always update a component fields on the UI goroutine!

Defer

func (c *Compo) Defer(fn func(Context))

Defer() is a Compo method that like Dispatch(), executes a given function on the UI goroutine. The difference is that the given function is executed only if the component is still mounted when it is called.

Here is an example where an HTTP request is performed when a page is loaded, and its result is stored in a component field with Defer():

type foo struct {
	app.Compo

	response []byte
}

func (f *foo) OnNav(ctx app.Context) {
	// Launching a new goroutine:
	ctx.Async(func() {
		r, err := http.Get("/bar")
		if err != nil {
			app.Log(err)
			return
		}
		defer r.Body.Close()

		b, err := ioutil.ReadAll(r.Body)
		if err != nil {
			app.Log(err)
			return
		}

		// Storing HTTP response in component field:
		f.Defer(func(app.Context) {
			f.response = b
			f.Update()
		})
	})
}

Next

Report issue

Found something incorrect, a typo or have suggestions to improve this article? Let me know :)

Loading go-app documentation...