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