年轻人的第一个 GO 桌面应用:用 Wails 做个学习搭子计时器

告别控制台应用,用Go语言打造你的第一个桌面软件

🎯 痛点场景:为什么需要桌面应用开发技能?

你是否遇到这些问题?

  • 学了Go语言,却只会写命令行工具?
  • 想学桌面开发,却觉得C++/C#太复杂?
  • 面试官对你的"烂大街"项目不感兴趣?

你能学到什么?

  • ✅️ Wails 基础使用
  • ✅️ 实时事件通信
  • ✅️ 系统托盘集成
  • ✅️ 多线程编程
  • ✅️ 附完整源码

🛠️ 环境准备

步骤1:安装Go语言

bash 复制代码
# 官网下载安装包:https://golang.org/dl/
go version  # 验证安装

步骤2:安装Wails框架

bash 复制代码
go install github.com/wailsapp/wails/v2/cmd/wails@latest
wails version  # 验证安装

步骤3:创建项目骨架

bash 复制代码
wails init -n Punktime
cd Punktime

🚀 核心功能开发

设计 UI (前端)

  • 使用 HTML/CSS/JavaScript实现:

    • 时间/倒计时主界面
    • 倒计时设置遮罩层
  • 示例代码

html 复制代码
<!-- ============================================================================
     * 主界面结构
     * 应用程序的主要显示区域
     * ============================================================================ -->

    <!-- 主容器:显示倒计时/时间 -->
    <div class="container">
      <div class="countdown" id="countdown"></div>
    </div>

    <!-- ============================================================================
     * 倒计时输入框模态对话框
     * 用于设置倒计时时长的弹出窗口
     * ============================================================================ -->

    <!-- 倒计时输入框遮罩层 -->
    <div
      id="countdownInputOverlay"
      class="countdown-input-overlay"
      style="display: none"
    >
      <div class="countdown-input-container">
        <div class="countdown-input-title"></div>
        <div>
          <!-- 分钟输入框 -->
          <input
            type="number"
            id="minutesInput"
            class="countdown-input"
            min="0"
            max="59"
            value="25"
            placeholder="分"
          />
          <span style="color: lightgreen">:</span>
          <!-- 秒数输入框 -->
          <input
            type="number"
            id="secondsInput"
            class="countdown-input"
            min="0"
            max="59"
            value="0"
            placeholder="秒"
          />
        </div>
        <div class="countdown-input-buttons">
          <!-- 确认按钮 -->
          <button id="confirmCountdown" class="countdown-input-button">
            确定
          </button>
          <!-- 取消按钮 -->
          <button id="cancelCountdown" class="countdown-input-button">
            取消
          </button>
        </div>
      </div>
    </div>

实现计时逻辑(Go后端)

  • 核心代码
Go 复制代码
/**
 * @description: 计时器更新循环,每秒执行一次的时间/倒计时更新逻辑
 * 函数作为计时器管理器的核心循环,负责:
 * 1. 每秒更新计时器状态和显示内容
 * 2. 根据当前显示模式(倒计时/时间)执行不同的更新逻辑
 * 3. 处理倒计时结束事件和状态转换
 * 4. 通过Wails事件系统向前端发送更新数据
 *
 * 倒计时模式逻辑:
 * - 运行中:计算剩余时间,倒计时结束时触发timerEnd事件
 * - 暂停中:显示暂停时的剩余时间
 * - 未开始:显示设置的倒计时总时长
 *
 * 时间模式逻辑:
 * - 显示当前系统时间(MM:SS格式)
 *
 * @example
 * // 每秒自动执行,无需手动调用
 * // 倒计时模式:显示"25:00" → "24:59" → ... → "00:00"(触发结束事件)
 * // 时间模式:显示当前时间如"14:30"
 *
 * @see runtime.EventsEmit 发送事件到前端
 * @see time.Since 计算时间间隔
 *
 * @note 使用互斥锁确保线程安全,避免并发访问计时器状态
 * @note 倒计时结束时自动重置状态并发送结束事件
 * @note 时间格式统一为两位数(如"05:09"而非"5:9")
 */
func (tm *TimerManager) updateLoop() {
    for range tm.ticker.C {
        tm.mu.Lock()
        if tm.isTimerRunning {
                tm.timerElapsed = time.Since(tm.timerStartTime)
        }
        switch tm.displayMode {
        case "countdown":
                if tm.isTimerRunning {
                        remaining := tm.countdownDuration - tm.timerElapsed
                        if remaining <= 0 {
                                remaining = 0
                                tm.isTimerRunning = true
                                tm.timerElapsed = tm.countdownDuration
                                tm.isPaused = false
                                runtime.EventsEmit(tm.ctx, "timerEnd")

                                timerText := "00:00"
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        } else {
                                minutes := int(remaining.Minutes())
                                seconds := int(remaining.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        }

                } else {
                        if tm.isPaused {
                                remaining := tm.countdownDuration - tm.timerElapsed
                                if remaining < 0 {
                                        remaining = 0
                                }
                                minutes := int(remaining.Minutes())
                                seconds := int(remaining.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        } else {
                                minutes := int(tm.countdownDuration.Minutes())
                                seconds := int(tm.countdownDuration.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        }
                }

        case "time":
                currentTime := time.Now().Format("15:04")
                runtime.EventsEmit(tm.ctx, "timeUpdate", currentTime)

        }
        tm.mu.Unlock()
}
}
  • 前后端通信
    • 前端负责监听更新事件并渲染
    • 后端负责触发更新事件并发送数据
js 复制代码
 // 监听倒计时更新事件
  window.runtime.EventsOn("timerUpdate", (data) => {
    document.getElementById("countdown").textContent = data;
  });
  // 监听时间显示更新事件
  window.runtime.EventsOn("timeUpdate", (data) => {
    document.getElementById("countdown").textContent = data;
  });
go 复制代码
if tm.isTimerRunning {
    remaining := tm.countdownDuration - tm.timerElapsed
    if remaining <= 0 {
            remaining = 0
            tm.isTimerRunning = true
            tm.timerElapsed = tm.countdownDuration
            tm.isPaused = false
            runtime.EventsEmit(tm.ctx, "timerEnd")

            timerText := "00:00"
            // 发送事件显示更新事件
            runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
    } else {
            minutes := int(remaining.Minutes())
            seconds := int(remaining.Seconds()) % 60
            timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
            // 发送事件显示更新事件
            runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
    }

📦️ 打包发布

bash 复制代码
wails build

📖 学习资源

👋你还希望有哪些学习搭子?欢迎留言!

❤️如果对你有帮助,别忘了点赞 + 关注!

相关推荐
梦想很大很大8 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰13 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘17 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤17 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想