Как создавать легкие платформонезависимые приложения на Go  -  без JS и BS

C помощью Go можно создавать как платформонезависимые приложения, так и настольные и мобильные.

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

С ними сокращается не только время до вывода на рынок, но и стоимость разработки: приложения создаются компаниями лишь раз, а запускаются везде.

Но что, если вы не крупная компания, цель которой  —  экономия денег, а разработчик Go с идеей создать запускаемый в любой ОС продукт с минимальным функционалом?

Возвращаться к JavaScript или изучать инструменты клиентских приложений JavaScript типа Vue и React?

Ни в коем случае: когда есть Go, не нужно ни это, ни чего-либо другое!

Благодаря сторонней библиотеке Gio платформонезависимые приложения для Linux, Windows, macOS, Android и iOS создаются полностью на Go без JS, HTML, CSS.

Итак, сделаем приложение GoGiggles с chatGPT для генерирования шуток о разработчиках Go. Почему именно Go? У них отличное чувство юмора. При этих словах разработчики Rust покинули чат.

Но сначала пара слов о Gio.

Что такое Gio?

Это библиотека с непосредственным режимом реализации графического интерфейса для создания легковесных приложений MacOs, Windows, Linux, FreeBSD, OpenBSD, Android, iOS и WebAssembly. Она и сама легковесна из-за малого числа зависимостей, проста в освоении и использовании.

В отличие от платформонезависимых фреймворков Electron и Wails с применением в них веб-технологий для интерфейса, приложения на Gio рисуются самой библиотекой, из-за чего меньше потребление памяти.

С момента выпуска Gio задействована в создании нескольких эффективных, производительных приложений, например легкого, платформонезависимого настольного криптокошелька Godcr для управления DCR-токенами и регулирования, а также Tailscale Android, Android-клиента с открытым исходным кодом для Tailscale  —  альтернативы ячеистой сети VPN.

Gio универсальна и не ограничивается этими сценариями. Благодаря надежному материальному пользовательскому интерфейсу и гибкой архитектуре на Gio создаются различные приложения под уникальные задачи и требования.

Что понадобится для создания приложения:

  • базовые знания Go;
  • Go 1.20;
  • ОС Windows, Linux или Mac.

Создание нового проекта Go

Сначала включаем модуль Go:

export GO111MODULE=on

А этими тремя командами:

mkdir go_giggles &&
cd go_giggles &&
go mod init go_giggles

создаем каталог go_giggles, переходим в него и создаем модуль Go go_giggles, настраивая в созданном каталоге новый проект.

Создав проект Go Giggles, добавим в его зависимости библиотеку Gio.

Установка Gio

Устанавливаем Gio:

go get gioui.org@latest

Этой командой добавляем Gio в файл go.mod и загружаем библиотеку в кеш модуля Go.

Создание первого приложения GioUI

Чтобы ознакомиться с методами, виджетами и функционалом Gio, создадим простое приложение Gio с приветственным сообщением, затем запустим приложение в ОС и добавим функционал chatGPT.

Сначала создаем в каталоге проекта файл main.go:

touch main.go

Добавляем в этот файл код:

package main

import (
"image/color"
"log"
"os"

"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/widget/material"
)

var theme = material.NewTheme(gofont.Collection())

func main() {
go func() {
w := app.NewWindow()
err := run(w)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}

func run(w *app.Window) error {
var ops op.Ops
for {
e := <-w.Events()
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)

title := material.H1(theme, "Hi, I'm Giggles")
maroon := color.NRGBA{R: 127, G: 0, B: 0, A: 255}
title.Color = maroon
title.Alignment = text.Middle
title.Layout(gtx)

e.Frame(gtx.Ops)
}
}
}

Пара незнакомых функций и методов? Сейчас разберемся.

Разбор кода

Создание окна

Каждому приложению с графическим интерфейсом обычно требуется минимум одно окно. Функцией main запускается цикл приложения для взаимодействия с ОС, в отдельной горутине инициируется логика окна:

func main() {
go func() {
w := app.NewWindow()
err := run(w)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}

В контексте окна выполняется логика конкретного приложения и обрабатываются любые ошибки, с завершением логики приложения процесс завершается.

Создание темы

Чтобы определить внешний вид приложения Gio, инициализируем тему для создания стилизованных виджетов. Этой строкой кода создаем новую тему с семейством шрифтов Go для всех текстовых элементов:

var theme = material.NewTheme(gofont.Collection())
...

Оно применяется в material.H1(theme, "Hi, I'm Giggles") функции run для создания стилизованного заголовка с текстом Hi, I'm Giggles.

Прослушивание событий

Этот блок кода  —  основной цикл событий окна для обработки в приложении Gio разных событий, таких как запросы перерисовки и удаление окна:

for {
e := <-w.Events()
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
...
}
}

При получении system.FrameEvent в приложении создается новый контекст макета, заполняемый определенными виджетами пользовательского интерфейса. В случае нашего приложения рисуется виджет заголовка H1.

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

Если получено system.DestroyEvent, окно приложения находится в процессе закрытия из-за возможного взаимодействия с пользователем, например нажатия значка закрытия. Любая ошибка, связанная с этим процессом, возвращается после на обработку.

Отрисовка текста

В этом блоке кода на событие системного фрейма прореагировано инициализацией нового контекста макета, созданием большой текстовой подписи, стилизованной темно-бордовым цветом, выравниванием текста по середине его области, перемещением его в графический контекст и отображением результата в графическом процессоре:

var ops op.Ops
for {
...
case system.FrameEvent:
// Этот контекст макета используется для управления состоянием рендеринга.
gtx := layout.NewContext(&ops, e)

// Определяется большая подпись с текстом:
title := material.H1(theme, "Hi, I'm Giggles")

// Меняется цвет подписи.
maroon := color.NRGBA{R: 127, G: 0, B: 0, A: 255}
title.Color = maroon

// Меняется положение подписи.
title.Alignment = text.Middle

// Подпись перемещается в контекст макета.
title.Layout(gtx)

// Операции рисования передаются в графический процессор.
e.Frame(gtx.Ops)
}
}

С кодом разобрались, теперь создадим и запустим это простое приложение Gio в операционной системе.

Создание и запуск приложения Gio в ОС

Для запуска может потребоваться установка зависимостей.

Linux

На Linux устанавливаем библиотеки Wayland, x11, xkbcommon, GLES, EGL и libXcursor:

apt install gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev

Для оптимальной производительности отдельно устанавливаем драйвер Vulkan. Понадобится он, например, в дистрибутиве Arch.

Установлен ли Vulkan, проверяем в терминале командой vulkaninfo: если драйвер настроен, вернется информация о версии его экземпляра.

Установив зависимости, запускаем приложение:

go mod tidy && go run .

Mac OS

Здесь нужен только xcode. Установив его, запускаемся:

go mod tidy && go run .

Windows

Для запуска приложения на Windows достаточно настроек по умолчанию:

go mod tidy && go run .

Если при запуске приложения Gio отображаются консоли, запускаемся такой командой:

go run -ldflags="-H windowsgui" .

Если все зависимости установлены корректно и приложение Gio запущено, появится приветственное окно:

Поздравляем, вы только что создали свое первое платформонезависимое приложение полностью на Go.

Теперь сделаем его интереснее.

Добавление функционала ChatGPT

Вызовем конечную точку chatGPT в OpenAI.

Чтобы получить секретный ключ OpenAI, создаем учетную запись на сайте OpenAI или входим в уже имеющуюся. На странице API-ключей создаем новый секретный ключ и копируем его в безопасное место. Созданный ключ появляется в списке на той же странице:

Секретный ключ не обнародуем: удаляем из кода, прежде чем коммитить и добавлять в публичный репозиторий вроде Git.

С помощью этого ключа в API chatGPT на OpenAI отправляются запросы. Интегрируем этот функционал в приложение.

Добавление функции для вызова конечной точки GPT

Чтобы добавить функцию для вызова конечной точки chatGPT, вместо стандартной библиотеки Go HTTP используем стороннюю go-gpt3: у нее меньше кода и проще реализация.

Извлекаем библиотеку в локальный кеш и включаем ее в файл go.mod такой командой терминала:

go get github.com/sashabaranov/go-openai

Добавляем в файл main.go функцию getGPTResponse:

func getGPTResponse(client *openai.Client, prompt string) (string, error) {
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: prompt,
},
},
},
)
if err != nil {
return "", err
}
return resp.Choices[0].Message.Content, nil
}

Функцией getGPTResponse принимается два аргумента: экземпляр клиента OpenAI и строка prompt. А после из модели ИИ возвращается сгенерированный текст или ошибка запроса.

С помощью указанного клиента и входных данных prompt в API отправляется запрос Chat Completion, в который включается сообщение от роли пользователя с содержимым подсказки.

Если запрос успешен, функцией из модели возвращается содержимое первого сообщения-ответа. При ошибке запроса первым значением возвращается пустая строка, вторым  —  ошибка.

Чтобы запрашивать через API chatGPT новые шутки, в приложении нужен интерактивный элемент. В идеале  —  кнопка. Добавим в GoGiggles кнопку, которой вызывается getGPTResponse, при каждом ее нажатии отображается новая шутка.

Добавление кнопки

Импортируем библиотеку виджетов Gio и инициализируем виджет кнопки, добавив в файл main.go такой код:

import (
.
.
"gioui.org/widget"
.
.
)

var (
theme = material.NewTheme(gofont.Collection())
button = new(widget.Clickable)
)

type (
C = layout.Context
D = layout.Dimensions
)

Переменная button  —  это экземпляр интерактивного виджета, которым при взаимодействии с пользователем запускается действие.

Псевдонимы типов C и D  —  это типы layout.Context и layout.Dimensions соответственно. Так к этим типам удобнее обращаться в коде.

Инициализируем переменной стандартный текст подписи для отображения шутки и новый клиент OpenAI, добавив в функцию run такой код:

var labelText = "Hi, I'm Giggles, I can tell you jokes about Go developers"
var client = openai.NewClient("open-ai-secret-key")

Заменяем строку open-ai-secret-key на секретный ключ OpenAI.

Теперь для отображения кнопки и виджетов макета меняем в функции run код макета, применяем вертикальный макет Flex и центрируем в нем label и button:

for {
.
.
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
lbl := material.Label(theme, unit.Sp(20), labelText)
lbl.Alignment = text.Middle
return lbl.Layout(gtx)
})
}),
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
btn := material.Button(theme, button, "Tell me a joke")
return btn.Layout(gtx)
})
}),
)
e.Frame(gtx.Ops)
.
.
}

Здесь в макете Flex имеется два дочерних блока flex, которыми центрируются элементы label и button: каждому элементу дается 50% доступного пространства. Первым дочерним элементом flexed отображается подпись для показа шуток, вторым  —  кнопки.

Чтобы при нажатии кнопки вызывалась функция getGPTResponse, добавим прослушиватель событий.

Обработка событий нажатия

Для обработки событий нажатия добавляем в case system.FrameEvent в функции run такой код:

if button.Clicked() {
go func() {
response, err := getGPTResponse(client, "Tell me a joke about Go developers")
if err != nil {
log.Printf("GPT Err: %s", err)
return
}
labelText = response
w.Invalidate()
}()
}

При нажатии кнопки создается горутина, которой для генерирования моделью ИИ GPT-3 шутки вызывается функция getGPTResponse.

Благодаря фоновому выполнению функции в горутине, событие нажатия кнопки продолжается без блокировки основного потока выполнения.

Если эта операция успешна и нет ошибки, шутка присваивается переменной labelText и для запуска события фрейма, которым в пользовательский интерфейс привносится новый текст, вызывается метод Invalidate.

Добавив эту часть кода, мы завершаем приложение GoGiggles.

Собираем все вместе

В итоге в файле main.go получается такой код:

package main

import (
"context"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/sashabaranov/go-openai"
"log"
"os"
)

var (
theme = material.NewTheme(gofont.Collection())
button = new(widget.Clickable)
)

type (
C = layout.Context
D = layout.Dimensions
)

func getGPTResponse(client *openai.Client, prompt string) (string, error) {
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT3Dot5Turbo,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleUser,
Content: prompt,
},
},
},
)
if err != nil {
return "", err
}
return resp.Choices[0].Message.Content, nil
}

func run(w *app.Window) error {
var ops op.Ops
var labelText = "Hi, I'm Giggles, I can tell you jokes about Go developers"
var client = openai.NewClient("open-ai-secret-key")
for {
e := <-w.Events()
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)

if button.Clicked() {
go func() {
response, err := getGPTResponse(client, "Tell me a joke about Go developers")
if err != nil {
log.Printf("GPT Err: %s", err)
return
}
labelText = response
w.Invalidate()
}()
}

layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
lbl := material.Label(theme, unit.Sp(20), labelText)
lbl.Alignment = text.Middle
return lbl.Layout(gtx)
})
}),
layout.Flexed(1, func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx C) D {
btn := material.Button(theme, button, "Tell me a joke")
return btn.Layout(gtx)
})
}),
)

e.Frame(gtx.Ops)
}
}
}

func main() {
go func() {
w := app.NewWindow()
err := run(w)
if err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
app.Main()
}

Запускаем приложение так же, как простую реализацию Gio, и видим:

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

Дополнительные возможности

Кроме того, благодаря обширному инструментарию Gio, дополнительным библиотекам и виджетам пользовательского интерфейса добавили функционал, сделав приложение GoGiggles интереснее.

Исходный код приложения GoGiggles доступен на GitHub. Используйте его как отправную точку для своих экспериментов с Gio.

Чтобы копировать шутки в буфер обмена, добавьте кнопку копирования. А чтобы делиться ими  —  кнопки соцсетей. Сделайте приложение для устройств iOS и Android.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jahdunsin Osho: Build Lightweight Cross-platform Applications Entirely in Go, no JS — no BS

Предыдущая статьяНовые API браузера, необходимые каждому веб-разработчику
Следующая статья8 продвинутых вопросов для собеседования по JavaScript