Функции и методы в языке 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", который проходит прямо сейчас во время публикации данного материала.

Комментарии (9)


  1. DeusModus
    16.05.2022 21:19
    +12

    Да, но зачем?


    1. Yeah
      18.05.2022 13:39

      Обфускация хитрой логики, которая составляет коммерческую тайну, например


      1. DeusModus
        18.05.2022 18:04

        В таком случае можно использовать сам JS/AssemblyScript, с которым знакомы фронты/фуллстеки. Все еще непонятно зачем это Go'шникам


    1. dmitriizolotov Автор
      18.05.2022 18:11

      Можно, например, использовать готовые go-модули, реализующие вычислительно сложные задачи (те же нейронки, например, или какую-либо обработку изображений/видео/звука). Ну и чтобы не миксовать с JS, визуализацию результатов можно собрать там же, на Go


  1. shaman4d
    16.05.2022 23:14

    Сложные интерфейся будут более отзывчивыми ?


  1. geebv
    17.05.2022 00:46

    По своему опыту вижу плюс в wasm если надо иметь например единую кодовую базу на фронте с бекендом на go. Либо для какой то специфики или скорости. Типа как в figma все на wasm+svg (вроде?) что бы все летало. Но не уверен на счет figma, где то слышал.


  1. ya_penek
    17.05.2022 18:11

    Все это конечно хорошо, но бинарники получаются слишком большие даже для современных скоростей интернета. Частично проблема решается использованием tinygo, но он не все умеет.

    Я тестил скорость WASM и обнаружил, что при работе с DOM объектами прирост по сравнению с JS незначительный. А тяжелая математика и криптография реально ускоряется.


    1. dmitriizolotov Автор
      17.05.2022 19:50

      DOM фактически проксируется через V8 (с ним общаемся через syscall/js), поэтому принципиальной разности в скорости работы с элементами не будет. В основном изменения в производительности заметны при реализации вычислительно сложных задач.

      А относительно размера можно посмотреть в сторону https://github.com/webassembly/binaryen, там есть оптимизирующий инструмент wasm-opt, который позволяет выполнять оптимизацию wasm уже после компиляции.


  1. DirectX
    18.05.2022 22:13
    +2

    Отличный мануал, очень в тему. Понятно, что использовать для создания веб фреймворка это вообще ИМХО нецелесообразно, но для ресурсоёмких вычислений или хитрых библиотек то, что надо. Собрал пример с капчей в более полном варианте, включая проверку: https://github.com/DirectX/go-wasm-captcha-demo