7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tauriは気になる。でもRustはつらい…という人へ。Goで作れるデスクトップアプリ開発フレームワーク「Wails」

Last updated at Posted at 2025-12-12

この記事はand factory.inc Advent Calendar 2025 13日目の記事です。

昨日はkuri_hara_kunさんの 最近ホットなランサムウェアについてまとめてみた でした!

Goでもデスクトップアプリを作りたい

クロスプラットフォームって言葉、かっこいいですよね。
デスクトップアプリにおけるクロスプラットフォーム開発といえば、古くはElectron、最近ではRustで書けてバンドルサイズが小さいTauriのようなフレームワークが人気です。

しかし、

  • Tauriは気になるけど、Rustは難しそう
  • かと言ってElectronはバンドルサイズが大きくてちょっと……

と思ったことはないですか?
そこで、普段業務で使っているGoで書けるデスクトップアプリ開発プラットフォームがないか探したところ、Wailsというものを見つけたので紹介します。

Wailsとは

WailsはGoでロジックを書き、Web技術(React/Vue)などでUIを作れるデスクトップアプリ開発フレームワークです。
Electronと同様にWeb技術をベースにしていますが、内部でChromiumとNode.jsを動かしているElectronと違い、OS組み込みのWebエンジン(WebView/WebKitなど)を利用するため、Electronと比べてバンドルサイズが大幅に小さく、起動も軽量なのが特徴です。

インストール

公式: https://wails.io/docs/gettingstarted/installation/

筆者の動作環境(記事執筆時)

  • macOS 26.0.1
  • Go 1.25.3
  • Wails v2.11.0
  1. Xcode コマンドラインツールのインストール(Macの場合)

    xcode-select --install
    
  2. インストールされていることの確認

    xcode-select -p
    
  3. Wails CLI のインストール

    go install github.com/wailsapp/wails/v2/cmd/wails@latest
    
  4. 正常にインストールできたか確認

    wails doctor
    
  5. Node も念のため入れておく(フロントエンドビルド用)

    brew install node
    

使ってみた

Wailsの雰囲気をつかむため、シンプルなToDoアプリを作ってみます。

最終的には以下のようなUIのものを作ります。
image.png

  • やりたいこと
    • 常駐アプリとして実装
    • SQliteによるToDoの永続化
    • CRUD
      • 作成
      • 取得
      • 更新(完了/未完了)
      • 削除
    • ショートカットでウィンドウの表示/非表示の切り替え

プロジェクト作成

プロジェクトの作成は下記のチュートリアルを参考に行います

今回はReact(TypeScript)テンプレートを使用します。

wails init -n hello-wails -t react-ts

生成される構造は以下の通り。

.
├── build/
│   ├── appicon.png
│   ├── darwin/
│   └── windows/
├── frontend/
├── go.mod
├── go.sum
├── main.go
└── wails.json

作業ディレクトリへ移動し、開発モードで起動します

cd hello-wails
wails dev

しばらくすると……

image.png
このような画面が出てきます。

テキストボックスに文字を入れてみると、挨拶が返ってきました。嬉しいですね
image.png

各ファイルについて

バックエンド

main.goに作成するアプリケーションの構成を記述します。
今回は画面の端に常駐するタイプのUIを作りたいので、ウィンドウ感をなくすためにFramelessAlwaysOnTopなどのOptionを設定しています

// main.go
package main

import (
	"embed"

	"github.com/wailsapp/wails/v2"
	"github.com/wailsapp/wails/v2/pkg/menu"
	"github.com/wailsapp/wails/v2/pkg/menu/keys"
	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/options/assetserver"
	"github.com/wailsapp/wails/v2/pkg/options/mac"
	"github.com/wailsapp/wails/v2/pkg/options/windows"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
	// Create an instance of the app structure
	app := NewApp()

	// Widget size: compact and positioned at right edge
	width := 380
	height := 600

	// Create menu with global shortcut
	appMenu := menu.NewMenu()
	toggleMenuItem := menu.Text("Toggle Window", keys.CmdOrCtrl("shift+t"), func(_ *menu.CallbackData) {
		app.ToggleWindow()
	})
	appMenu.Append(toggleMenuItem)

	// Create application with options
	err := wails.Run(&options.App{
		Title:  "Todo Widget",
		Width:  width,
		Height: height,
		AssetServer: &assetserver.Options{
			Assets: assets,
		},
		BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},
		Frameless:        true,
		AlwaysOnTop:      true,
		Menu:             appMenu,
		OnStartup:        app.startup,
		OnDomReady:       app.onDomReady,
		Bind: []interface{}{
			app,
		},
		Mac: &mac.Options{
			TitleBar: mac.TitleBarHiddenInset(),
		},
		Windows: &windows.Options{
			Theme: windows.Theme(windows.Dark),
		},
	})

	if err != nil {
		println("Error:", err.Error())
	}
}

app.goにはバックエンド側のビジネスロジックを記述します。
DBの初期化処理やToDoの操作などのAPIはここで呼び出しています。
ウィンドウが表示される際に画面の端に固定するなどの処理もこのファイルで行います。

ここで定義した構造体をmain.goでインスタンス化することでフロントにメソッドを公開できます。

// app.go
package main

import (
	"context"
	"hello-wails/backend/api"
	"hello-wails/backend/db"
	"hello-wails/backend/models"
	"hello-wails/backend/repository"
	"hello-wails/backend/usecase"

	"github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
	ctx       context.Context
	TodoApi   *api.TodoApi
	isVisible bool
}

// NewApp creates a new App application struct
func NewApp() *App {
	return &App{}
}

// startup runs on application start
func (a *App) startup(ctx context.Context) {
	// DB 接続など初期化処理(詳細な実装は割愛)
	database, _ := db.Connect()
	database.AutoMigrate(&models.Todo{})

	repository := repository.NewTodoRepository(database)
	usecase := usecase.NewTodoUsecase(repository)

	a.TodoApi = api.NewTodoApi(usecase)
	a.ctx = ctx
	a.isVisible = true
}

// Todo API delegations (詳細な実装は割愛)

func (a *App) GetAll() ([]api.TodoResponse, error) {
	// Todo の一覧取得処理(詳細な実装は割愛)
	return a.TodoApi.GetAll()
}

func (a *App) GetById(id uint) (*api.TodoResponse, error) {
	// 指定 ID の Todo を取得(詳細な実装は割愛)
	return a.TodoApi.GetById(id)
}

func (a *App) Create(todo *models.Todo) error {
	// Todo 作成処理(詳細な実装は割愛)
	return a.TodoApi.Create(todo)
}

func (a *App) Update(todo *models.Todo) error {
	// Todo 更新処理(詳細な実装は割愛)
	return a.TodoApi.Update(todo)
}

func (a *App) Delete(id uint) error {
	// Todo 削除処理(詳細な実装は割愛)
	return a.TodoApi.Delete(id)
}

func (a *App) ToggleDone(id uint) error {
	// 完了/未完了の切り替え(詳細な実装は割愛)
	return a.TodoApi.ToggleDone(id)
}

// Window control

func (a *App) ToggleWindow() {
	// ウィンドウの表示/非表示切り替え(詳細な実装は割愛)
	if a.isVisible {
		runtime.WindowHide(a.ctx)
		a.isVisible = false
	} else {
		runtime.WindowShow(a.ctx)
		runtime.WindowSetAlwaysOnTop(a.ctx, true)
		a.isVisible = true
	}
}

func (a *App) onDomReady(ctx context.Context) {
	// ウィンドウの初期位置調整(詳細な実装は割愛)
	screens, _ := runtime.ScreenGetAll(ctx)
	if len(screens) > 0 {
		screen := screens[0]
		x := screen.Width - 380
		y := (screen.Height - 600) / 2
		runtime.WindowSetPosition(ctx, x, y)
		runtime.WindowSetAlwaysOnTop(ctx, true)
	}
}

フロントエンド

WailsではフロントエンドにReact/Vueなど一般的なWeb技術をそのまま利用できます。

今回はReact(TypeScript)を使用したため、
frontend/ 配下には通常の Vite + React プロジェクトとほぼ同じ構造が生成されます。

frontend/
├── src/
│   ├── App.tsx
│   ├── main.tsx
│   ├── wailsjs/   ← 自動生成される Wails 連携ファイル
│   └── ...
├── index.html
└── package.json

特にポイントとなるのはfrontend/wailsjs/に生成されるファイル群です。
ここにはGoバックエンドで公開した関数と、TypeScript型定義が自動生成されます。

これにより、フロントからGoの処理を安全に呼び出せる仕組みが提供されます。

例えばToDo一覧を取得したい場合、React側では次のようなコードになります。

import { useState, useEffect } from "react";
import { GetAll } from "../wailsjs/go/main/App";
import * as models from "../wailsjs/go/models";

// Wails 自動生成の ToDo 型を使用
type Todo = models.Todo;

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);

  // --- loadTodos(GetAll を呼ぶだけの最小例) ---
  const loadTodos = async () => {
    const allTodos = await GetAll();
    setTodos(allTodos);
  };

  useEffect(() => {
    loadTodos();

    // --- ショートカット設定や他の処理は中略 ---
    // (中略)
  }, []);

  // --- UI の他の部分は中略 ---
  // (中略)

  return (
    <div>
      <h2>Todo List</h2>

      {todos.length === 0 ? (
        <p>Todo がありません</p>
      ) : (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              {todo.title} {todo.done ? "" : ""}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

HTTP 通信や API サーバは不要で、
Go の関数がそのまま TypeScript の関数として呼べる のが Wails の大きな特徴です。

アプリのビルド

以下のコマンドを実行するとbuild/binディレクトリにバイナリが保存されます

wails build

出力されたhello-wails.appを実行してみると……
wails.gif

うごきました!
出力されたバイナリのサイズを確認したところ、11.5MBと、シンプルなアプリとはいえElectron製のアプリよりもかなりコンパクトで嬉しいです

まとめ

普段の業務ではGoでWebAPIを書いていますが、Wailsを使うといつもとほとんど同じ構成・同じ書き心地でデスクトップアプリが作れました。

バックエンドのロジックはいつも通りGoで書けて、フロント側との接続部分はWailsがよしなに自動生成してくれるため、「APIを叩くためのHTTP実装」や「型の同期」などをほぼ意識せずに開発できる点が非常に快適でした。

デスクトップアプリを作りたいけれど、
「Electronは重いし…」「Rustを学ぶのはハードルが高い…」
と感じているWebエンジニアの方には、Wailsはかなりお勧めできるんじゃないかな、と思います。

Goの知識をそのまま活かしつつ、軽量で扱いやすいデスクトップアプリを作りたい人は、ぜひ Wails を試してみてください。

備忘録

dev環境だと動くのにビルドしたバイナリを実行した際に正常に起動しない問題が発生したのでメモ

ToDoを保存するためにSQLiteを使用しているのですが、dbファイルのパスを相対パスで書いていたため、ビルド後に実行場所が変わり、
DBファイルが見つからない → panic → 即終了
で落ちていたようです

以下のように絶対パスを使うように書き換えることで修正し他ところ、起動するようになりました

//修正前
package db

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func Connect() (*gorm.DB, error) {
	db, err := gorm.Open(sqlite.Open("todo.db"), &gorm.Config{})
	if err != nil {
		return nil, err
	}
	return db, nil
}
//修正後
package db

import (
	"os"
	"path/filepath"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func Connect() (*gorm.DB, error) {
	path, _ := os.Executable()
	dbfile := filepath.Join(filepath.Dir(path), "todo.db")
	db, err := gorm.Open(sqlite.Open(dbfile), &gorm.Config{})
	if err != nil {
		return nil, err
	}
	return db, nil
}
7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?