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.
Читайте также:
- Что возвращать в Go: структуры или интерфейсы?
- Бесперебойный API на Golang
- Как я встраивал ресурсы в Go
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jahdunsin Osho: Build Lightweight Cross-platform Applications Entirely in Go, no JS — no BS