Функции и методы в языке golang

Наиболее известным набором инструментов для компиляции в wasm32 является emscripten, с его помощью можно скомпилировать приложение, написанное на C/C++ или на любом языке, имеющим frontend-компилятор для LLVM. При этом компилятор подменяет вызовы OpenGL и POSIX на соответствующие аналоги в браузере, что например используется при компиляции библиотеки skia для браузера (canvaskit) из исходного кода на C++, а также портирование существующих библиотек (например, ffmpeg или opencv). Но некоторые языки программирования поддерживают wasm32 как одну из целевых платформ, среди которых можно выделить Kotlin (Native) и Go. В этой статье мы обсудим общие вопросы о запуске приложений Go в среде браузера и использование библиотеки Vecty для создания веб-приложений на основе переиспользуемых компонентов.

Компиляция в целевую платформу wasm32 поддерживается в Go, для этого нужно указать значения переменных окружения GOOS=js и GOARCH=wasm. Начнем с самой простой программы и постепенно будет усложнять возможности нашего веб-приложения.

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, WebAssembly World!")
}

Выполним компиляцию исходного кода в wasm-байткод:

GOOS=js GOARCH=wasm go build -o hello.wasm

И далее создадим сценарий для загрузки и запуска wasm-файла в HTML:

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="wasm_exec.js"></script>
        <script>
            if (WebAssembly) {
                 const go = new Go();
                 WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
                    go.run(result.instance);
                 });
            } else {
               console.log("WebAssembly is not supported in your browser")
            }
        </script>
    </head>
    <body></body>
</html>

Файл wasm_exec.js может быть получен, например, отсюда или из установки Go (по расположению misc/wasm). Он создает необходимый контекст для выполнения приложения на Go (например, создает реализации для syscall/js, который мы будем использовать для взаимодействия с браузером, а также регистрирует прототип Go, который будет использоваться для начальной загрузки байткода и запуска функции main.

Для обхода ограничений безопасности браузера (по умолчанию загрузка сетевых ресурсов запрещена, если страница открыта через локальную ссылку в схеме file://) запустим docker-контейнер с nginx.

docker run --name gotest -d -p 80:80 -v `pwd`:/usr/share/nginx/html nginx

После обращения к localhost можно увидеть, что сообщение выводится в консоль браузера. Теперь давайте подключимся к DOM и попробуем вызвать Javascript-функцию из нашего wasm-приложения. Для доступа к JS необходимо подключить модуль syscall/js и использовать глобальный контекст для доступа к зарегистрированным символам в js через вызов Global() для получения доступа к объекту window. Например, для получения текущего адреса можно использовать следующее выражение.

js.Global().Get("location").Get("href")

Для вызова функций из Javascript или встроенных в браузер можно использовать метод Call от объекта (или от глобального контекста). Например, для вывода диалога уведомления можно использовать вызов:

js.Global().Call("alert", "Hello, WASM")

Для добавления элемента в DOM можно комбинировать вызовы, через использование объекта document:

document := js.Global().Get("document")
body := document.Call("querySelector", "body")
div := document.Call("createElement", "div")
div.Set("innerHTML", "Hello, WASM")
body.Call("appendChild", div)

Аналогично можно вызвать JS-функции из кода на Go. В html-файле в <script> добавим новую функцию log с одним параметром.

function log(s) {
  console.log('Log from JS: ['+s+']');
}

И выполним обращение к ней из кода на Go:

js.Global().Call("log", "Message from Go")

Также можно экспортировать функцию из Go и сделать ее доступной в Javascript, для этого через функцию Set регистрируется функция через js.FuncOf. Сигнатура функции в общем виде должна возвращать значение произвольного типа и принимать список аргументов и ссылку на себя:

func function_name(this js.Value, inputs []js.Value) interface{}

Например, таким образом можно обернуть функции работы с изображениями в Go и сделать примитивный генератор капчи на стороне клиента.

package main

import (
	"bytes"
	"encoding/base64"
	captcha "github.com/dchest/captcha"
	"syscall/js"
)

func generateCaptcha(this js.Value, inputs []js.Value) interface{} {
	id := captcha.New()
	img_buf := bytes.NewBufferString("")
	captcha.WriteImage(img_buf, id, 128, 64)
	return "data:image/png;base64,"+base64.StdEncoding.EncodeToString(img_buf.Bytes())
}

func main() {
	js.Global().Set("captcha", js.FuncOf(generateCaptcha))
	<-make(chan bool)
}

Чтение из пустого канала в последней строке main необходимо, чтобы избежать завершения кода на wasm с удалением всех зарегистрированных символов. Теперь в любом месте после вызова функции main (запуска приложения через go.run) мы можем обратиться к зарегистрированной функции и получить изображение капчи:

img = document.createElement("img");
img.src = captcha();
document.body.appendChild(img);

Если нам нужно передать сложную структуру (например, отправить одновременно изображение и голосовую капчу), то здесь мы встретимся с проблемой, что структуры напрямую в JS не отправляются (возникает ошибка panic: ValueOf: invalid value). Одним из путей решения проблемы может быть сериализация в JSON или иные формы упаковки структур в единый объект, например Protobuf).

Таким образом, мы можем использовать готовые библиотеки, разработанные для Go внутри нашего браузера. Но создавать сайты таким способом крайне сложно и неэффективно и равноценно использованию. Обратим свое внимание теперь на библиотеки для создания динамических веб-приложений на Go.

Библиотека Vecty

Vecty во многом схож с подходами реактивных пользовательских интерфейсов на React и предлагает похожую модель компонентов, зависящих от состояния. Каждый компонент использует структуру vecty.Core и реализует метод Render, формирующий дерево элементов с использованием оберток вокруг HTML-тэгов (в hexops/vecty/elem), а также любых других компонентов, созданных в приложении. При определении компонента могут быть заданы дополнительные парам��тры, определяющие его состояние. Важным отличием от React является необходимость уведомления об изменении состояния, через вызов метода Rerender (при этом может быть перестроено не все приложение, а только часть дерева).

Например, компонент может быть определен следующим образом:

type ScreenView struct {
	vecty.Core
	id int
}

func (p *ScreenView) Render() vecty.ComponentOrHTML {
	return elem.Div(
		vecty.Markup(
			vecty.Class("screen"+strconv.Itoa(p.id)),
			vecty.MarkupIf(powerOn && active == p.id, vecty.Class("selected"))),
		elem.Canvas(
			vecty.Markup(
				prop.ID("canvas"+strconv.Itoa(p.id)),
				vecty.Style("background", "black"),
				vecty.Property("width", strconv.Itoa(screenWidth)),
				vecty.Property("height", strconv.Itoa(screenHeight)),
			),
		),
	)
}

Здесь состояние определяется глобальной переменной powerOn и внутренним идентификатором id. В vecty.Markup могут использоваться следующие структуры:

  • prop.ID - изменить значение свойства id для тэга;

  • prop.Name - изменить значение свойства name для тэга;

  • vecty.Class - значение атрибута class для тэга;

  • vecty.MarkupIf - условная вставка значений (или тэга), будет выполнена только при истинности первого аргумента;

  • vecty.Style - для переопределения атрибута style тэга;

  • vecty.Property - изменение произвольного поля объекта, связанного с HTML-тэгом;

  • vecty.Attribute - изменение произвольного атрибута тэга;

  • vecty.Data - изменение data-атрибутов тэга;

  • vecty.EventListener - для регистрации обработчика событий;

  • vecty.UnsafeHTML - вставка произвольного HTML-фрагмента.

В дальнейшем созданные компоненты могут быть включены в другие компоненты.

type Screens struct {
	vecty.Core
}

func (p *Screens) Render() vecty.ComponentOrHTML {
	return elem.Div(vecty.Markup(vecty.Class("centered")),
		elem.Div(
			vecty.Markup(vecty.Class("screens")),
			&LeftButton{},
			&ScreenView{id: 0},
			&ScreenView{id: 1},
			&ScreenView{id: 2},
			&ScreenView{id: 3},
			&RightButton{},
		),
	)
}

При необходимости регистрации обработчика событий можно интегрировать в vecty.Markup структуру EventListener:

return elem.Data(vecty.Markup(
		vecty.Class("fa-button"),
		vecty.Class("right"),
		&vecty.EventListener{Name: "click", Listener: func(event *vecty.Event) {
      powerOn = !powerOn
      vecty.Rerender(emulator)
		}}), vecty.Text("\uF054"))

Важно, что обновление состояние может выполняться асинхронно внутри goroutine, например после выполнения длительного сетевого запроса или выполнения сложных и длительных алгоритмических задач в Go.

Для создания приложений доступна готовая обертка для использования Material Design Components https://github.com/vecty-components/material.

Среди альтернативных решений можно назвать Go-App (реализует очень похожую на Vecty модель переиспользуемых компонентов), Tango (использует подход, сходный с AngularJS), Vugu (близка по используемой модели Vue.js).

Пример кода приложения с использованием Vecty можно посмотреть в Github: https://github.com/AirCube-Project/emulator/.

Также хочу пригласить всех желающих на бесплатный урок по теме: "Функции и методы в языке golang". Регистрация доступна по этой ссылке.

А тут можно посмотреть запись урока "Структуры языка golang", который проходит прямо сейчас во время публикации данного материала.