WebAssembly позволяет запускать в браузере код низкоуровневых языков, таких как C, C++, Rust и Go. Мы компилируем Go-код в байт-код, и когда инстанцируем его в теге html-скрипта или JS-коде, среда выполнения браузера создает виртуальную машину для выполнения кода WebAssembly (wasm). Для распараллеливания выполнения WASM с основным потоком может использоваться веб-воркер.
Виртуальная машина задействует компилятор JUST IN TIME, который преобразует байт-код в машинный, а также может производить оптимизацию в зависимости от аппаратного обеспечения. WASM безопасен, поскольку работает в среде “песочницы” и не может получить доступ к аппаратному обеспечению базовой системы.
Когда мы загружаем или инстанцируем wasm, мы получаем модуль, который является пакетом Go-кода. При инстанцировании создается экземпляр wasm, являющийся отдельным контекстом выполнения модуля.
Хватит теории — перейдем к коду.
Создание проекта
Сначала нужно создать проект Golang. Для этого выполним следующую команду.
go mod init wasmgo
Go-код
Добавление функции
Создайте файл main.go со следующим содержимым:
package main
import (
"fmt"
"syscall/js"
)
// создание json-функции, совместимой с WebAssembly
func add() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 2 {
return "Invalid number of argument"
}
// Примечание: конвертация типов важна, поскольку операторы не перегружены для js.Value
// Вы получите сообщение "invalid operation: operator + not defined on arg1 (variable of type js.Value)".
arg1 := args[0].Int()
arg2 := args[1].Int()
return arg1 + arg2
})
}
// регистрация обратных вызовов, совместимых с js
func registerCallbacks() {
// вызов функции и задание ей имени, которое будет использоваться в js
js.Global().Set("add", add())
}
func main() {
fmt.Println("running go as webassembly")
registerCallbacks()
// нужно добавить канал и вызвать его, чтобы программа Golang не закрылась, иначе получим следующую ошибку:
// Uncaught (in promise) Error: Go program has already exited
<-make(chan bool)
}
Мы создали функцию add, которая будет возвращать JS-функцию, используя JS-объект, импортированный из syscall/js. Цель — создать функцию, которая будет принимать аргументы из JS-кода и использовать их в Go, затем выполнять операцию и возвращать результат, который может быть использован в JS.
Мы определяем функцию и ожидаем массив []js.Value в качестве аргументов. Нам нужно преобразовать их в соответствующий тип Golang, так как JS не чувствителен к типам. Это можно сделать следующим образом: args[0].Int().
Теперь можем выполнить добавление и вернуть результат.
Регистрация
Нам также необходимо зарегистрировать функцию, чтобы ее можно было вызывать из JS. Даем имя функции и настраиваем ее для использования в браузере.
js.Global().Set("add", add())
Функция main
Осталось только вызвать функцию register в main, но нам также необходимо убедиться, что Go-код не закроется после запуска. Поэтому воспользуемся пустым каналом. Теперь Go-код готов.
func main() {
fmt.Println("running go as webassembly")
registerCallbacks()
// нужно добавить канал и вызвать его, чтобы программа Golang не закрылась, иначе получим следующую ошибку:
// Uncaught (in promise) Error: Go program has already exited
<-make(chan bool)
}
Сборка
Для сборки Go-кода необходимо указать архитектуру и операционную систему, для которой производится сборка, а затем создать сборку с расширением wasm.
GOARCH=wasm GOOS=js go build -o main.wasm main.go
Фронтенд
WebAssembly
Нам необходимо инициализировать WebAssembly с помощью файла main.wasm. После этого запускаем Go-код в этом экземпляре. Можем написать следующий код в отдельном js-файле.
// создание экземпляра Go
const go = new Go();
// запуск экземляра WebAssembly
WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject)
.then(
result => {
// запуск экземпляра Go wasm
go.run(result.instance)
// видим результат в консоли браузера
console.log("Summition happening in Golang")
}
)
HTML
Нужно создать фронтенд приложения, чтобы лучше его представить, но на самом деле реальная работа происходит в тегах script html-файла. Добавьте путь к JS-файлу, который мы только что написали на html — в данном случае это go.js.
<!DOCTYPE html>
<html>
<head>
<title>Web Assembly</title>
// перейти к js -> предоставляется самим Golang
<script src="wasm_exec.js"></script>
// инициализация wasm
<script src="go.js"></script>
<script>
let summision = () => {
let resultElement = document.querySelector("#result")
let input1 = document.querySelector("#arg1").value
let input2 = document.querySelector("#arg2").value
console.log("input1: ", input1)
console.log("input2: ", input2)
resultElement.innerHTML = add(Number(input1), Number(input2))
}
</script>
</head>
<body>
<input id="arg1" />
<input id="arg2" />
<button onclick="summision()">add</button>
<h1>Sumission is: <span id="result"></span></h1>
</body>
</html>
Переход к JS
У Golang есть два пути, которые настраиваются при его установке: один содержит все проекты, которые мы устанавливаем, а другой — путь, по которому установлен Golang.
// место установки Golang
export PATH=$PATH:/usr/local/go/bin
// сюда попадают все загруженные проекты, это домашний каталог
export GOPATH=$(go env GOPATH)
Cкопируем файл wasm_exec.js из /usr/local/go/misc/wasm в домашний каталог проекта и укажем его путь в теге script.
<script src="wasm_exec.js"></script>
Результат
Откройте localhost:8000, введите несколько чисел, и Golang добавит их за вас.
Общие ошибки
1. Если импорт “syscall/js” остается красным, это означает, что проблема не в коде, а в расширении.
2. Если запускать код не от сервера, то можно получить ошибку следующего вида:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at file:///home/raotalha/Code/TestCode/WebAssembly/main.wasm. (Reason: CORS request not http).
Uncaught (in promise) TypeError: NetworkError when attempting to fetch resource.
3. Преобразование типов важно, потому что операторы не перегружены для js.Value, иначе вы получите следующее: invalid operation: operator + not defined on arg1 (variable of type js.Value).
4. Вызовите функцию и задайте ей имя, которое будет использоваться в js, иначе получите ошибку js.Global().Set(“add”, add()).
5. Чтобы Golang-программа не закрылась, нужно добавить канал и вызвать его, иначе получим Uncaught (in promise) Error: Go program has already exited.
Читайте также:
- WebAssembly на Golang с нуля
- WebAssembly: секретное оружие в разработке высокооптимизированных и безопасных веб-приложений
- Возможности и перспективы WebAssembly
Читайте нас в Telegram, VK и Дзен
Перевод статьи Rao Talha: Web Assembly in Golang