go-GUI框架:fyne教程及解决中文乱码等常见bug
1 fyne教程
- fyne教程地址:
- https://www.topgoer.cn/docs/goday/goday-1crdp17nj4v6p
- https://pkg.go.dev/fyne.io/fyne/v2#section-readme
1.1 介绍
- 简单易用,fyne提供了简单直观的API,可以快速学习并上手进行开发
- 跨平台支持,fyne提供了多个平台的支持,包括:Windows、MacOs、Linux等
- 响应式布局,提供了灵活的布局管理器,我们可以根据需要动态调整页面元素的位置和大小
- 丰富的组件库,fyne内置许多常用且美观的组件库,大大降低了一个GUI的开发成本
- 可扩展性高,允许我们自定义渲染器、主题和样式,最大程度上的对页面进行DIY
- 事件处理强大,提供了丰富的事件处理机制和系统托盘,我们可以通过监听用户输入、按钮点击等事件来实现与用户的交互
- 社区活跃和文档丰富,fyne拥有一个活跃的社区和丰富的文档教程等,如果遇到bug可以在一定程度上寻求解决办法
注意:
fyne不支持go1.12以下的go版本,同时不支持32位的windows XP系统
1.2 使用(项目实战:GUI客户端+托盘)
下面将用fyne2实现系统托盘,并且点击托盘不同菜单展示不同页面
项目目录结构:
①main.go
项目入口
package main
import (
"github.com/flopp/go-findfont"
"os"
"strings"
"ziyi.com/ziyi-guard/ui"
"ziyi.com/ziyi-guard/ui/service"
)
func init() {
//设置中文字体:解决中文乱码问题
fontPaths := findfont.List()
for _, path := range fontPaths {
if strings.Contains(path, "msyh.ttf") || strings.Contains(path, "simhei.ttf") || strings.Contains(path, "simsun.ttc") || strings.Contains(path, "simkai.ttf") {
os.Setenv("FYNE_FONT", path)
break
}
}
}
func main() {
args := os.Args
if len(args) >= 2 {
arg := args[1]
//根据参数做对应操作
service.SelectXGuard(arg)
} else {
app := ui.NewGuardApp()
app.W.ShowAndRun()
}
}
②sys_service.go
注册windows服务
package service
import (
"fmt"
"github.com/kardianos/service"
"log"
"os"
"time"
"ziyi.com/ziyi-guard/consts"
)
var (
// 创建一个ServiceConfig对象,用于描述服务的配置信息
svcConfig = &service.Config{
Name: consts.ServiceName,
DisplayName: consts.ServiceDisplayName,
Description: consts.ServiceDescription,
Arguments: consts.Arguments,
Option: consts.Options,
}
// 创建一个Program对象
prog = &Program{exit: make(chan struct{})}
s service.Service
)
func init() {
// 将Program对象与ServiceConfig对象绑定,并创建一个新的Service对象
svc, err := service.New(prog, svcConfig)
if err != nil {
log.Fatalf("init service failed...err=%v", err)
}
s = svc
}
// Program 结构体定义了实现Service接口所需的方法
type Program struct {
exit chan struct{}
}
// Start 是在service.Start方法调用时被自动调用的方法
// 在启动服务时执行具体的业务代码
func (p *Program) Start(s service.Service) error {
go p.run()
return nil
}
// Stop 是在service.Stop方法调用时被自动调用的方法
func (p *Program) Stop(s service.Service) error {
close(p.exit)
return nil
}
func SelectXGuard(arg string) {
// 如果命令行参数为install、start、stop或restart,则执行对应的操作
// 如果没有命令行参数,则输出命令行帮助信息
switch arg {
case "install":
err := s.Install()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s installed.", s.String())
case "start":
err := s.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s started.", s.String())
case "stop":
err := s.Stop()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s stopped.", s.String())
case "uninstall":
err := s.Uninstall()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s uninstall.", s.String())
case "run":
err := s.Run()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s run.", s.String())
case "restart":
err := s.Stop()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s stopped.", s.String())
err = s.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s started.", s.String())
default:
log.Printf("Usage: %s install|uninstall|start|stop|restart", os.Args[0])
}
}
func (p *Program) run() {
file, err := os.OpenFile("E:\\Go\\GoPro\\src\\go_code\\work\\ziyi-guard\\log.txt", os.O_WRONLY|os.O_RDONLY, os.ModePerm)
defer file.Close()
if err != nil {
log.Println("open file err=", err)
return
}
for {
time.Sleep(time.Second * 5)
file.WriteString(fmt.Sprintf(time.Now().String()) + " hello\n")
}
}
③data_check.go
数据格式校验等
package ui
import (
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"github.com/aobco/log"
"os/exec"
"regexp"
"strings"
"time"
)
/*
数据合法性校验、URL连通性校验等
*/
var (
isConnected = make(chan bool, 1)
urlRegex = `^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,5})$`
)
// 数据合法性校验
func DataCheck(w fyne.Window, data *GuardData) error {
url := data.url
name := data.name
tags := data.tags
//校验 IP:Port 格式
r := regexp.MustCompile(urlRegex)
ok := r.MatchString(url)
if !ok || url == "" {
dialog.NewError(errors.New("url格式有误,请按照:【192.168.145.13:7777】格式填写"),
w,
).Show()
return errors.New("数据格式有误")
}
if name == "" {
dialog.NewError(errors.New("name格式有误,输入内容不能为空"),
w,
).Show()
return errors.New("数据格式有误")
}
if tags == "" {
dialog.NewError(errors.New("tags格式有误,请按照:【--port=8082, --url=www.baidu.com 】格式填写\n(参数之间使用逗号分割)"),
w,
).Show()
return errors.New("数据格式有误")
}
return nil
}
// 通过执行 ping 命令检测指定 IP 是否可以 ping 通
func PingIp(ip string, w fyne.Window) bool {
start := time.Now().Second()
cmd := exec.Command("ping", ip)
go func() {
log.Infof("检测iP....%v", ip)
w.SetContent(widget.NewLabel("checking ... URL....please don't close the page, otherwise the application will crash\n" +
"检测URL连通性中,请不要关闭该页面,否则将引起页面崩溃..."))
w.Show()
}()
err := cmd.Run()
end := time.Now().Second()
log.Infof("check ip cost time :%v", end-start)
if err != nil {
log.Infof("check ip , connect the target fail, err=%s", err)
isConnected <- false
return false
}
isConnected <- true
return true
}
func ConnectTarget(url string, w fyne.Window) error {
log.Infof("检测ziyi-guard配置目标 URl连通性,url:%v", url)
ip := strings.Split(url, ":")[0]
isConnected = make(chan bool, 1)
PingIp(ip, w)
select {
case flag := <-isConnected:
if flag {
close(isConnected)
} else {
err := errors.New("connect the target failed error")
return err
}
}
return nil
}
④func_file.go
对文件进行操作
package ui
import (
"bufio"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"github.com/aobco/log"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"ziyi.com/ziyi-guard/consts"
"ziyi.com/ziyi-guard/ui/config"
)
/*
对guardData.cong文件操作
*/
func init() {
wd, _ := os.Getwd()
filename = filepath.Dir(wd) + consts.XGuardConfRelativePath
}
var (
filename string
guardConfig = config.NewDefaultGuardConfig()
)
// 从配置文件读取信息
func ReadConfFile() (string, error) {
file, err := os.Open(filename)
if err != nil {
log.Infof("打开ziyi-guard配置文件失败 filename=%s error=%s", filename, err)
return "", err
}
defer file.Close()
if _, err := os.Stat(filename); err != nil {
if os.IsNotExist(err) {
log.Infof("xguard配置文件不存在,xguard.conf...")
if _, err := os.Create(filename); err != nil {
log.Infof("新建xguard.conf配置文件失败, error=%s", err)
return "", err
}
}
}
reader := bufio.NewReader(file)
tmpSlice := make([]string, 3, 3)
for {
str, err := reader.ReadString('\n')
if str != "" {
str = strings.Replace(str, "\n", "", -1)
str = strings.TrimSpace(str)
}
tmpSlice = append(tmpSlice, str)
if err == io.EOF {
break
}
}
guardResData := ""
for _, v := range tmpSlice {
if v != "" {
guardResData += v
guardResData += separator
}
}
return guardResData, nil
}
// 保存配置信息到文件[write]
func WriteConfFile(data string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = fmt.Fprint(file, data)
if err != nil {
return err
}
log.Infof("保存xguard配置文件信息:%s", data)
return nil
}
// 从配置文件读取Agent信息
func ReadAllAgentInfo(myWindow fyne.Window) []string {
//选项个数根据配置文件个数来
dirPath, _ := os.ReadDir(guardConfig.AgentDirPath)
dirCount := len(dirPath)
options := make([]string, dirCount)
index := 1
for {
if index <= dirCount {
options = append(options, fmt.Sprintf("agent%v", index))
index++
} else {
break
}
}
index = 0
choices := make([]string, dirCount)
for _, v := range options {
if v != "" {
choices[index] = v
index++
}
}
log.Info("Agent下拉框选项:%v", choices)
// 创建下拉框
selectEntry := widget.NewSelectEntry(choices)
selectEntry.PlaceHolder = "please choose an agent"
// 创建左侧固定区域
left := container.NewVBox(
widget.NewLabel("Select an option:"),
selectEntry,
)
// 创建右侧区域
right := container.NewVBox(
widget.NewLabel("agent basic information"),
widget.NewLabel(""),
)
// 创建水平分割容器,并设置分割比例
split := container.NewHSplit(left, right)
split.SetOffset(0.25)
// 设置右侧标签的文本为下拉框的当前值
selectEntry.OnChanged = func(s string) {
fileName := guardConfig.AgentDirPath + "\\" + s + ".conf"
fmt.Println(fileName)
data, err := readConfig(fileName)
if err != nil {
//dialog.NewInformation("info", err.Error(), myWindow).Show()
fmt.Println("err=", err)
}
right.Objects[1].(*widget.Label).SetText(data)
}
myWindow.SetContent(split)
myWindow.Resize(fyne.Size{Height: 500, Width: 800})
return choices
}
func readConfig(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
contentBytes, err := ioutil.ReadAll(file)
if err != nil {
return "", err
}
content := string(contentBytes)
return content, nil
}
⑤guard_desk.go
构建系统托盘
package ui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/desktop"
"github.com/aobco/log"
"io/ioutil"
"os"
"path/filepath"
"ziyi.com/ziyi-guard/consts"
)
/*
ziyi-guard系统托盘图标构建
*/
func init() {
wd, _ := os.Getwd()
iconPath = filepath.Dir(wd) + consts.IconRelativePath
}
var (
iconPath string
)
type GuardDesk struct {
trayIcon fyne.Resource
trayMenu fyne.Menu
}
func InitGuardDesk(guardApp GuardApp) error {
if desk, ok := guardApp.A.(desktop.App); ok {
//获取托盘与图标
menu := NewGuardTrayMenu(guardApp.W)
iconData, err := TransIconToByte()
if err != nil {
log.Infof("获取弹框图标失败,err=", err)
return err
}
desk.SetSystemTrayMenu(menu)
desk.SetSystemTrayIcon(fyne.NewStaticResource("ziyi-guard", iconData))
}
return nil
}
// 将.ico转换为字节数组
func TransIconToByte() ([]byte, error) {
file, err := os.Open(iconPath)
if err != nil {
log.Infof("转换icon失败 open file err=%s", err)
return nil, err
}
bytes, err := ioutil.ReadAll(file)
if err != nil {
log.Infof("转换icon失败 read icon err=%s", err)
return nil, err
}
return bytes, nil
}
⑥tray_menu.go
托盘对应菜单点击事件
package ui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"github.com/aobco/log"
"net"
"strings"
)
type GuardTrayMenu struct {
}
func NewGuardTrayMenu(w fyne.Window) *fyne.Menu {
m := fyne.NewMenu("ziyi-guard",
fyne.NewMenuItem("配置ziyi-guard", func() {
w.Show()
}),
fyne.NewMenuItem("查看配置信息", func() {
w.SetTitle("ziyi-guard信息查看")
MakeGuardView(w)
w.Show()
}),
fyne.NewMenuItem("获取本地IP", func() {
w.SetTitle("本地IP地址")
MakeIpView(w)
w.Show()
}),
fyne.NewMenuItem("启动服务", func() {
StartService(w)
}),
fyne.NewMenuItem("停止服务", func() {
StopService(w)
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("Agent信息", func() {
//查看Agent信息
ShowAgentInfo(w)
w.Show()
}),
)
return m
}
// 查看Guard配置信息
func MakeGuardView(w fyne.Window) {
data, err := ReadConfFile()
if err != nil {
log.Infof("读取ziyi-guard配置文件失败 err=", err)
}
w.SetTitle("ziyi-guard配置信息")
split := strings.Split(data, "####")
str := "暂无配置信息"
if len(split) != 1 {
form := widget.NewForm(
widget.NewFormItem("URL:", widget.NewLabel(split[0])),
widget.NewFormItem("Name:", widget.NewLabel(split[1])),
widget.NewFormItem("Tags:", widget.NewLabel(split[2])),
)
content := container.NewVBox(form)
w.SetContent(content)
} else {
content := widget.NewLabel(str)
w.SetContent(content)
}
}
// 查看IP页面
func MakeIpView(w fyne.Window) {
ipAddress := GetLocalIp()
content := widget.NewLabel("本地IP地址:" + ipAddress)
w.SetContent(content)
}
// 获取本地IP地址
func GetLocalIp() string {
interfaces, err := net.Interfaces()
if err != nil {
log.Info(err)
return ""
}
for _, iface := range interfaces {
if strings.Contains(iface.Name, "Win Adapter") {
addrs, err := iface.Addrs()
if err != nil {
log.Info(err)
continue
}
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip := ipnet.IP.String()
log.Infof("获取本地IP成功:%s", ip)
if !strings.HasPrefix(ip, "169.254") {
return ip
}
}
}
}
}
}
return ""
}
// 启动服务
func StartService(w fyne.Window) {
//TODO 接收服务启动成功消息
w.Show()
dialog.NewInformation("信息", "启动服务成功", w).Show()
//复原mainMenu
MakeMainUI(w, GuardData{
url: "",
name: "",
tags: "",
})
}
// 停止服务
func StopService(w fyne.Window) {
//TODO 接收服务停止消息
w.Show()
dialog.NewInformation("信息", "停止服务成功", w).Show()
MakeMainUI(w, GuardData{
url: "",
name: "",
tags: "",
})
}
// 构建页面【查看所有Agent信息】
func ShowAgentInfo(w fyne.Window) {
//读取agent配置信息
ReadAllAgentInfo(w)
}
⑦main_window.go
UI系统主页面
package ui
import (
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"github.com/aobco/log"
"net"
"strings"
)
/*
ziyi-guard配置主页面
*/
type GuardData struct {
url string
name string
tags string
}
var (
separator = "####"
//ipRegex = `((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}`
)
// MakeMainUI
func MakeMainUI(myWindow fyne.Window, guardData GuardData) {
myWindow.SetTitle("ziyi-guard主页面")
// 创建输入选项卡
urlEntry := widget.NewEntry()
nameEntry := widget.NewEntry()
tagEntry := widget.NewEntry()
//数据回显
if guardData.url != "" {
urlEntry.Text = guardData.url
}
if guardData.name != "" {
nameEntry.Text = guardData.name
}
if guardData.tags != "" {
tagEntry.Text = guardData.tags
}
// 创建
data, err := ReadConfFile()
if err != nil {
log.Infof("读取ziyi-guard配置文件失败:%s", err)
}
split := strings.Split(data, separator)
log.Infof("读取ziyi-guard配置文件成功:%s", split)
str := "暂无配置信息"
var showTab *container.TabItem
tmpUrl := ""
if len(split) != 1 {
form := widget.NewForm(
widget.NewFormItem("URL:", widget.NewLabel(split[0])),
widget.NewFormItem("Name:", widget.NewLabel(split[1])),
widget.NewFormItem("Tags:", widget.NewLabel(split[2])),
)
//配置页面数据回显
urlEntry.Text = split[0]
nameEntry.Text = split[1]
tagEntry.Text = split[2]
tmpUrl = split[0]
content := container.NewVBox(form)
showTab = container.NewTabItem("查看配置", content)
} else {
urlEntry.SetPlaceHolder("例:192.168.145.13:7777")
nameEntry.SetPlaceHolder("例:ziyi-guard守护")
tagEntry.SetPlaceHolder("例:--port=8082, -C=/usr/local/guard")
content := widget.NewLabel(str)
showTab = container.NewTabItem("查看配置", content)
}
//监听点击事件
submitButton := widget.NewButton("提交", func() {
url := urlEntry.Text
name := nameEntry.Text
tags := tagEntry.Text
log.Infof("用户提交ziyi-guard配置信息 url:%s,name:%s,tags:%s\n", url, name, tags)
//更新showTab内容
url = urlEntry.Text
name = nameEntry.Text
tags = tagEntry.Text
//tmpUrl用于connectBtn对连通性进行检测
if tmpUrl == "" || url != "" {
tmpUrl = url
}
//数据合法性校验
err = DataCheck(myWindow, &GuardData{
url: url,
name: name,
tags: tags,
})
if err != nil {
//清空输入框[用户体验:不清空]
MakeMainUI(myWindow, GuardData{
url: url,
name: name,
tags: tags,
})
return
}
form := widget.NewForm(
widget.NewFormItem("URL:", widget.NewLabel(url)),
widget.NewFormItem("Name:", widget.NewLabel(name)),
widget.NewFormItem("Tags:", widget.NewLabel(tags)),
)
showTab.Content = container.NewVBox(form)
//保存数据到文件
WriteConfFile(url + "\n" + name + "\n" + tags + "\n")
dialog.NewInformation("信息",
"更新数据成功",
myWindow,
).Show()
MakeMainUI(myWindow, GuardData{
url: url,
name: name,
tags: tags,
})
//清空内容
urlEntry.SetText("")
nameEntry.SetText("")
tagEntry.SetText("")
})
submitButton.Importance = widget.HighImportance
//检测URL连通性
connectBtn := widget.NewButton("检测URL连通性", func() {
text := urlEntry.Text
if text != "" {
tmpUrl = text
tmpIp := strings.Split(text, ":")[0]
//正则匹配
address := net.ParseIP(tmpIp)
if address == nil {
log.Infof("tmpIp:%v", tmpIp)
dialog.NewError(errors.New("请输入正确的IP格式"), myWindow).Show()
return
}
}
err = ConnectTarget(tmpUrl, myWindow)
if err != nil {
dialog.NewError(errors.New("连接到目标端失败,请更换URL"), myWindow).Show()
} else {
dialog.NewInformation("成功", "连接到目标端成功", myWindow).Show()
}
MakeMainUI(myWindow, GuardData{
url: urlEntry.Text,
name: nameEntry.Text,
tags: tagEntry.Text,
})
})
submitTab := container.NewTabItem("配置", container.NewVBox(
widget.NewLabel("URL:"),
urlEntry,
widget.NewLabel("Name:"),
nameEntry,
widget.NewLabel("tags:"),
tagEntry,
connectBtn,
submitButton,
))
// 创建选项卡容器
tabs := container.NewAppTabs(submitTab, showTab)
// 将选项卡容器添加到窗口中并显示
myWindow.SetContent(tabs)
myWindow.Resize(fyne.NewSize(float32(500.0), float32(500.0)))
}
⑧guard_app.go
构建fyne2的app
package ui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"github.com/aobco/log"
)
/*
*
ziyi-guard的UI界面构建
*/
type GuardApp struct {
A fyne.App
W fyne.Window
}
func NewGuardApp() *GuardApp {
myApp := &GuardApp{}
a := app.New()
//主题设置
a.Settings().SetTheme(theme.DarkTheme())
w := a.NewWindow("配置信息")
myApp.W = w
iconData, err := TransIconToByte()
if err != nil {
log.Infof("获取弹框图标失败, err=%s", err)
}
//弹框图标、位置、大小
icon := fyne.NewStaticResource("ziyi-guard", iconData)
w.SetIcon(icon)
w.CenterOnScreen()
w.Resize(fyne.NewSize(float32(500.0), float32(500.0)))
//系统托盘
systemTrayMenu := NewGuardTrayMenu(w)
desk := a.(desktop.App)
desk.SetSystemTrayMenu(systemTrayMenu)
desk.SetSystemTrayIcon(icon)
//设置拦截器
w.SetCloseIntercept(func() {
MakeMainUI(w, GuardData{
url: "",
name: "",
tags: "",
})
})
//主页面
MakeMainUI(w, GuardData{
url: "",
name: "",
tags: "",
})
//设置拦截
w.SetCloseIntercept(func() {
w.Hide()
MakeMainUI(w, GuardData{
url: "",
name: "",
tags: "",
})
})
return &GuardApp{
A: a,
W: w,
}
}
bug
①解决中文乱码问题(编译打包可用)
- 导入库
import "github.com/flopp/go-findfont"
- 添加初始化代码
func init() {
//设置中文字体
fontPaths := findfont.List()
for _, path := range fontPaths {
if strings.Contains(path, "msyh.ttf") || strings.Contains(path, "simhei.ttf") || strings.Contains(path, "simsun.ttc") || strings.Contains(path, "simkai.ttf") {
os.Setenv("FYNE_FONT", path)
break
}
}
}
2 注册windows服务
package main
import (
"fmt"
"github.com/kardianos/service"
"os"
)
func main() {
srvConfig := &service.Config{
Name: "MyGoService",
DisplayName: "MyGoService服务",
Description: "this is a service about go",
}
prg := &program{}
s, err := service.New(prg, srvConfig)
if err != nil {
fmt.Println(err)
}
if len(os.Args) > 1 {
serviceAction := os.Args[1]
switch serviceAction {
case "install":
err := s.Install()
if err != nil {
fmt.Println("安装服务失败: ", err.Error())
} else {
fmt.Println("安装服务成功")
}
return
case "uninstall":
err := s.Uninstall()
if err != nil {
fmt.Println("卸载服务失败: ", err.Error())
} else {
fmt.Println("卸载服务成功")
}
return
case "start":
err := s.Start()
if err != nil {
fmt.Println("运行服务失败: ", err.Error())
} else {
fmt.Println("运行服务成功")
}
return
case "stop":
err := s.Stop()
if err != nil {
fmt.Println("停止服务失败: ", err.Error())
} else {
fmt.Println("停止服务成功")
}
return
}
}
err = s.Run()
if err != nil {
fmt.Println(err)
}
}
type program struct{}
func (p *program) Start(s service.Service) error {
fmt.Println("服务运行...")
go p.run()
return nil
}
func (p *program) run() {
// 具体的服务实现
}
func (p *program) Stop(s service.Service) error {
return nil
}
添加上启动参数:
package main
import (
"fmt"
"github.com/kardianos/service"
"log"
"os"
"time"
)
// Program 结构体定义了实现Service接口所需的方法
type Program struct {
exit chan struct{}
}
// Start 是在service.Start方法调用时被自动调用的方法
// 在启动服务时执行具体的业务代码
func (p *Program) Start(s service.Service) error {
go p.run()
return nil
}
// Stop 是在service.Stop方法调用时被自动调用的方法
func (p *Program) Stop(s service.Service) error {
close(p.exit)
return nil
}
func main() {
// 创建一个ServiceConfig对象,用于描述服务的配置信息
svcConfig := &service.Config{
Name: "MyService",
DisplayName: "MyService",
Description: "This is a service for Guard.",
Arguments: []string{"run"},
Option: service.KeyValue{
"StartType": "automatic",
"OnFailure": service.OnFailureRestart,
"OnFailureDelayDuration": "1m",
"OnFailureResetPeriod": 10,
},
}
// 创建一个Program对象
prog := &Program{exit: make(chan struct{})}
// 将Program对象与ServiceConfig对象绑定,并创建一个新的Service对象
s, err := service.New(prog, svcConfig)
if err != nil {
log.Fatal(err)
}
// 如果命令行参数为install、start、stop或restart,则执行对应的操作
// 如果没有命令行参数,则输出命令行帮助信息
switch os.Args[1] {
case "install":
err = s.Install()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s installed.", s.String())
case "start":
err = s.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s started.", s.String())
case "stop":
err = s.Stop()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s stopped.", s.String())
case "uninstall":
err = s.Uninstall()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s uninstall.", s.String())
case "run":
err = s.Run()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s run.", s.String())
case "restart":
err = s.Stop()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s stopped.", s.String())
err = s.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Service %s started.", s.String())
default:
log.Printf("Usage: %s install|uninstall|start|stop|restart", os.Args[0])
}
}
func (p *Program) run() {
file, err := os.OpenFile("E:\\Go\\GoPro\\src\\go_code\\work\\ziyi-Guard\\log.txt", os.O_WRONLY|os.O_RDONLY, os.ModePerm)
defer file.Close()
if err != nil {
log.Println("open file err=", err)
return
}
for {
time.Sleep(time.Second * 5)
file.WriteString(fmt.Sprintf(time.Now().String()) + " hello\n")
}
}
3 tips
3.1去掉.exe运行的黑窗口
go build -ldflags "-s -w -H=windowsgui"
-s 省略符号表和调试信息
-w Omit the DWARF symbol table 省略DWARF符号表
-H windowsgui 不打印信息到console (On Windows, -H windowsgui writes a "GUI binary" instead of a "console binary."),就不会有cmd窗口了
3.2让exe程序以管理员身份运行(获取UAC)
在windows上执行有关系统设置命令的时候需要管理员权限才能操作,比如修改网卡的禁用、启用状态。双击执行是不能正确执行命令的,只有右键以管理员身份运行才能成功。
- UAC:用户账户控制
① 不带图标
- 安装rsrc工具
go get github.com/akavel/rsrc
- nac.manifest 文件拷贝到当前windows项目根目录
nac.manifest:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="9.0.0.0"
processorArchitecture="x86"
name="myapp.exe"
type="win32"
/>
<description>myapp</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
- 执行rsrc命令生成.syso文件
rsrc -manifest nac.manifest -o nac.syso
- build文件
build的时候不带任何参数,即:go build
go build
如果写成指定文件编译–go build main.go 将无法成功获取UAC。(go build 在编译开始时,会搜索当前目录的 go 源码以及.syso文件,最后将所有资源一起打包到EXE文件。go build main.go 这种指定文件的编译命令,会编译指定文件和指定文件里面的所需要的依赖包,但是不会将.syso 文件打包到EXE。)如果,你的golang程序需要UAC权限或带GUI界面的,一定要注意正确使用编译命令!
最后的目录结构:
② 带图标
-
安装rsrc
-
将icon.ico图标文件放在与main.go文件相同的文件夹下
-
编写main.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="9.0.0.0"
processorArchitecture="x86"
name="myapp.exe"
type="win32"
/>
<description>myapp</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
- 运行命令,生成.syso文件
rsrc -arch amd64 -manifest main.manifest -ico main.ico -o main.syso
需要加上arch amd64,不然回报 is incompatible with i386:x86-64 output 错误
- 编译打包文件
go build -ldflags "-s -w" -o xguard.exe
最后目录结构:
注意:如果报错的话,可能是因为没有gcc环境,下载并配置即可
- 下载链接:gcc下载地址
4 go-bindata使用
有时候我们需要将静态资源打包进.exe文件中,除了go后面官方提供的embed,还可以使用第三方组件:go-bindata
4.1 安装
1. go get -u github.com/go-bindata/go-bindata/...
// 引入fs(go-bindata-assetfs:提供fs服务)
2. go get github.com/elazarl/go-bindata-assetfs/...
4.2 打包生成bindata.go
# 将assets目录下的静态资源打包生成bindata/bindata.go文件中
go-bindata -o bindata/bindata.go -pkg bindata assets/...
4.3 main.go中引用bindata.go数据
项目结构:
func_file.go:
package service
import (
"bytes"
"compress/gzip"
"fmt"
"github.com/aobco/log"
"github.com/getlantern/systray"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"ziyi.com/xguard/bindata"
)
func init() {
// 注册静态文件处理函数
http.HandleFunc("/", serveStaticFile)
}
// serveStaticFile 返回静态文件内容
func serveStaticFile(w http.ResponseWriter, r *http.Request) {
// 获取文件路径
filePath := filepath.Join(staticDir, r.URL.Path)
log.Info("filePath:", filePath)
// 从 bindata.go 文件中读取文件内容
file, err := bindata.Asset(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 设置 Content-Type
contentType := "text/html"
if filepath.Ext(filePath) == ".css" {
contentType = "text/css"
} else if filepath.Ext(filePath) == ".js" {
contentType = "application/javascript"
}
w.Header().Set("Content-Type", contentType)
// 返回静态文件内容
fmt.Fprint(w, string(file))
}
# 编译
go build main.go
参考:https://blog.csdn.net/mirage003/article/details/127581356