关闭

Go实战--使用golang开发Windows Gui桌面程序(lxn/walk)

标签: golangguiWindows桌面程序
5945人阅读 评论(0) 收藏 举报
分类:

生命不止,继续 go go go!!!

golang官方并没有提供Windows gui库,但是今天还是要跟大家分享一下使用golang开发Windows桌面程序,当然又是面向github编程了。

知乎上有一个问答:
golang为什么没有官方的gui包?

这里,主要使用第三方库lxn/walk,进行Windows GUI编程。

lxn/walk

github地址:
https://github.com/lxn/walk

star:
2018

描述:
A Windows GUI toolkit for the Go Programming Language

获取:

go get github.com/lxn/walk

例子:

main.go

package main

import (
    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
    "strings"
)

func main() {
    var inTE, outTE *walk.TextEdit

    MainWindow{
        Title:   "SCREAMO",
        MinSize: Size{600, 400},
        Layout:  VBox{},
        Children: []Widget{
            HSplitter{
                Children: []Widget{
                    TextEdit{AssignTo: &inTE},
                    TextEdit{AssignTo: &outTE, ReadOnly: true},
                },
            },
            PushButton{
                Text: "SCREAM",
                OnClicked: func() {
                    outTE.SetText(strings.ToUpper(inTE.Text()))
                },
            },
        },
    }.Run()
}

go build生成go_windows_gui.exe。

go_windows_gui.exe.manifest

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
        <assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/>
        <dependency>
            <dependentAssembly>
                <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
            </dependentAssembly>
        </dependency>
    </assembly>

运行结果:
这里写图片描述

什么是manifest

上面提到了manifest,这是干什么的呢?

https://msdn.microsoft.com/en-us/library/windows/desktop/aa375365(v=vs.85).aspx

介绍:
Manifests are XML files that accompany and describe side-by-side assemblies or isolated applications. Manifests uniquely identify the assembly through the assembly’s assemblyIdentity element. They contain information used for binding and activation, such as COM classes, interfaces, and type libraries, that has traditionally been stored in the registry. Manifests also specify the files that make up the assembly and may include Windows classes if the assembly author wants them to be versioned. Side-by-side assemblies are not registered on the system, but are available to applications and other assemblies on the system that specify dependencies in manifest files.

是一种xml文件,标明所依赖的side-by-side组建。

如果用VS开发,可以Set通过porperty->configuration properties->linker->manifest file->Generate manifest To Yes来自动创建manifest来指定系统的和CRT的assembly版本。

详解
观察上面的manifest文件:

<xml>这是xml声明:

版本号----<?xml version="1.0"?>。
这是必选项。 尽管以后的 XML 版本可能会更改该数字,但是 1.0 是当前的版本。

编码声明------<?xml version="1.0" encoding="UTF-8"?>
这是可选项。 如果使用编码声明,必须紧接在 XML 声明的版本信息之后,并且必须包含代表现有字符编码的值。

standalone表示该xml是不是独立的,如果是yes,则表示这个XML文档时独立的,不能引用外部的DTD规范文件;如果是no,则该XML文档不是独立的,表示可以用外部的DTD规范文档。

<dependency>这一部分指明了其依赖于一个库:

<assemblyIdentity>属性里面还分别是:
type----系统类型,
version----版本号,
processorArchitecture----平台环境,
publicKeyToken----公匙

应用

做一个巨丑无比的登录框

这里用到了LineEdit、LineEdit控件

package main

import (
    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
)

func main() {
    var usernameTE, passwordTE *walk.LineEdit
    MainWindow{
        Title:   "登录",
        MinSize: Size{270, 290},
        Layout:  VBox{},
        Children: []Widget{
            Composite{
                Layout: Grid{Columns: 2, Spacing: 10},
                Children: []Widget{
                    VSplitter{
                        Children: []Widget{
                            Label{
                                Text: "用户名",
                            },
                        },
                    },
                    VSplitter{
                        Children: []Widget{
                            LineEdit{
                                MinSize:  Size{160, 0},
                                AssignTo: &usernameTE,
                            },
                        },
                    },
                    VSplitter{
                        Children: []Widget{
                            Label{MaxSize: Size{160, 40},
                                Text: "密码",
                            },
                        },
                    },
                    VSplitter{
                        Children: []Widget{
                            LineEdit{
                                MinSize:  Size{160, 0},
                                AssignTo: &passwordTE,
                            },
                        },
                    },
                },
            },

            PushButton{
                Text:    "登录",
                MinSize: Size{120, 50},
                OnClicked: func() {
                    if usernameTE.Text() == "" {
                        var tmp walk.Form
                        walk.MsgBox(tmp, "用户名为空", "", walk.MsgBoxIconInformation)
                        return
                    }
                    if passwordTE.Text() == "" {
                        var tmp walk.Form
                        walk.MsgBox(tmp, "密码为空", "", walk.MsgBoxIconInformation)
                        return
                    }
                },
            },
        },
    }.Run()
}

效果:
这里写图片描述

TableView的使用

这里主要使用的是TableView控件,代码参考github:

package main

import (
    "fmt"
    "sort"

    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
)

type Condom struct {
    Index   int
    Name    string
    Price   int
    checked bool
}

type CondomModel struct {
    walk.TableModelBase
    walk.SorterBase
    sortColumn int
    sortOrder  walk.SortOrder
    items      []*Condom
}

func (m *CondomModel) RowCount() int {
    return len(m.items)
}

func (m *CondomModel) Value(row, col int) interface{} {
    item := m.items[row]

    switch col {
    case 0:
        return item.Index
    case 1:
        return item.Name
    case 2:
        return item.Price
    }
    panic("unexpected col")
}

func (m *CondomModel) Checked(row int) bool {
    return m.items[row].checked
}

func (m *CondomModel) SetChecked(row int, checked bool) error {
    m.items[row].checked = checked
    return nil
}

func (m *CondomModel) Sort(col int, order walk.SortOrder) error {
    m.sortColumn, m.sortOrder = col, order

    sort.Stable(m)

    return m.SorterBase.Sort(col, order)
}

func (m *CondomModel) Len() int {
    return len(m.items)
}

func (m *CondomModel) Less(i, j int) bool {
    a, b := m.items[i], m.items[j]

    c := func(ls bool) bool {
        if m.sortOrder == walk.SortAscending {
            return ls
        }

        return !ls
    }

    switch m.sortColumn {
    case 0:
        return c(a.Index < b.Index)
    case 1:
        return c(a.Name < b.Name)
    case 2:
        return c(a.Price < b.Price)
    }

    panic("unreachable")
}

func (m *CondomModel) Swap(i, j int) {
    m.items[i], m.items[j] = m.items[j], m.items[i]
}

func NewCondomModel() *CondomModel {
    m := new(CondomModel)
    m.items = make([]*Condom, 3)

    m.items[0] = &Condom{
        Index: 0,
        Name:  "杜蕾斯",
        Price: 20,
    }

    m.items[1] = &Condom{
        Index: 1,
        Name:  "杰士邦",
        Price: 18,
    }

    m.items[2] = &Condom{
        Index: 2,
        Name:  "冈本",
        Price: 19,
    }

    return m
}

type CondomMainWindow struct {
    *walk.MainWindow
    model *CondomModel
    tv    *walk.TableView
}

func main() {
    mw := &CondomMainWindow{model: NewCondomModel()}

    MainWindow{
        AssignTo: &mw.MainWindow,
        Title:    "Condom展示",
        Size:     Size{800, 600},
        Layout:   VBox{},
        Children: []Widget{
            Composite{
                Layout: HBox{MarginsZero: true},
                Children: []Widget{
                    HSpacer{},
                    PushButton{
                        Text: "Add",
                        OnClicked: func() {
                            mw.model.items = append(mw.model.items, &Condom{
                                Index: mw.model.Len() + 1,
                                Name:  "第六感",
                                Price: mw.model.Len() * 5,
                            })
                            mw.model.PublishRowsReset()
                            mw.tv.SetSelectedIndexes([]int{})
                        },
                    },
                    PushButton{
                        Text: "Delete",
                        OnClicked: func() {
                            items := []*Condom{}
                            remove := mw.tv.SelectedIndexes()
                            for i, x := range mw.model.items {
                                remove_ok := false
                                for _, j := range remove {
                                    if i == j {
                                        remove_ok = true
                                    }
                                }
                                if !remove_ok {
                                    items = append(items, x)
                                }
                            }
                            mw.model.items = items
                            mw.model.PublishRowsReset()
                            mw.tv.SetSelectedIndexes([]int{})
                        },
                    },
                    PushButton{
                        Text: "ExecChecked",
                        OnClicked: func() {
                            for _, x := range mw.model.items {
                                if x.checked {
                                    fmt.Printf("checked: %v\n", x)
                                }
                            }
                            fmt.Println()
                        },
                    },
                    PushButton{
                        Text: "AddPriceChecked",
                        OnClicked: func() {
                            for i, x := range mw.model.items {
                                if x.checked {
                                    x.Price++
                                    mw.model.PublishRowChanged(i)
                                }
                            }
                        },
                    },
                },
            },
            Composite{
                Layout: VBox{},
                ContextMenuItems: []MenuItem{
                    Action{
                        Text:        "I&nfo",
                        OnTriggered: mw.tv_ItemActivated,
                    },
                    Action{
                        Text: "E&xit",
                        OnTriggered: func() {
                            mw.Close()
                        },
                    },
                },
                Children: []Widget{
                    TableView{
                        AssignTo:         &mw.tv,
                        CheckBoxes:       true,
                        ColumnsOrderable: true,
                        MultiSelection:   true,
                        Columns: []TableViewColumn{
                            {Title: "编号"},
                            {Title: "名称"},
                            {Title: "价格"},
                        },
                        Model: mw.model,
                        OnCurrentIndexChanged: func() {
                            i := mw.tv.CurrentIndex()
                            if 0 <= i {
                                fmt.Printf("OnCurrentIndexChanged: %v\n", mw.model.items[i].Name)
                            }
                        },
                        OnItemActivated: mw.tv_ItemActivated,
                    },
                },
            },
        },
    }.Run()
}

func (mw *CondomMainWindow) tv_ItemActivated() {
    msg := ``
    for _, i := range mw.tv.SelectedIndexes() {
        msg = msg + "\n" + mw.model.items[i].Name
    }
    walk.MsgBox(mw, "title", msg, walk.MsgBoxIconInformation)
}

效果:
这里写图片描述

文件选择器(加入了icon)

这里就是调用Windows的文件选择框
主要是使用:FileDialog

package main

import (
    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
)

import (
    "fmt"
    "os"
)

type MyMainWindow struct {
    *walk.MainWindow
    edit *walk.TextEdit

    path string
}

func main() {
    mw := &MyMainWindow{}
    MW := MainWindow{
        AssignTo: &mw.MainWindow,
        Icon:     "test.ico",
        Title:    "文件选择对话框",
        MinSize:  Size{150, 200},
        Size:     Size{300, 400},
        Layout:   VBox{},
        Children: []Widget{
            TextEdit{
                AssignTo: &mw.edit,
            },
            PushButton{
                Text:      "打开",
                OnClicked: mw.pbClicked,
            },
        },
    }
    if _, err := MW.Run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

}

func (mw *MyMainWindow) pbClicked() {

    dlg := new(walk.FileDialog)
    dlg.FilePath = mw.path
    dlg.Title = "Select File"
    dlg.Filter = "Exe files (*.exe)|*.exe|All files (*.*)|*.*"

    if ok, err := dlg.ShowOpen(mw); err != nil {
        mw.edit.AppendText("Error : File Open\r\n")
        return
    } else if !ok {
        mw.edit.AppendText("Cancel\r\n")
        return
    }
    mw.path = dlg.FilePath
    s := fmt.Sprintf("Select : %s\r\n", mw.path)
    mw.edit.AppendText(s)
}

效果:
这里写图片描述

文本检索器

package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
)

func main() {
    mw := &MyMainWindow{}

    if _, err := (MainWindow{
        AssignTo: &mw.MainWindow,
        Title:    "SearchBox",
        Icon:     "test.ico",
        MinSize:  Size{300, 400},
        Layout:   VBox{},
        Children: []Widget{
            GroupBox{
                Layout: HBox{},
                Children: []Widget{
                    LineEdit{
                        AssignTo: &mw.searchBox,
                    },
                    PushButton{
                        Text:      "检索",
                        OnClicked: mw.clicked,
                    },
                },
            },
            TextEdit{
                AssignTo: &mw.textArea,
            },
            ListBox{
                AssignTo: &mw.results,
                Row:      5,
            },
        },
    }.Run()); err != nil {
        log.Fatal(err)
    }

}

type MyMainWindow struct {
    *walk.MainWindow
    searchBox *walk.LineEdit
    textArea  *walk.TextEdit
    results   *walk.ListBox
}

func (mw *MyMainWindow) clicked() {
    word := mw.searchBox.Text()
    text := mw.textArea.Text()
    model := []string{}
    for _, i := range search(text, word) {
        model = append(model, fmt.Sprintf("%d检索成功", i))
    }
    log.Print(model)
    mw.results.SetModel(model)
}

func search(text, word string) (result []int) {
    result = []int{}
    i := 0
    for j, _ := range text {
        if strings.HasPrefix(text[j:], word) {
            log.Print(i)
            result = append(result, i)
        }
        i += 1
    }
    return
}

效果:
这里写图片描述

邮件群发器

别人写的邮件群发器,出自:
https://studygolang.com/articles/2078
关于golang中stmp的使用,请看博客:
Go实战–通过net/smtp发送邮件(The way to go)

package main

import (
    "bufio"
    "encoding/gob"
    "errors"
    "fmt"
    "io"
    "net/smtp"
    "os"
    "strconv"
    "strings"
    "time"
)

import (
    "github.com/lxn/walk"
    . "github.com/lxn/walk/declarative"
)

type ShuJu struct {
    Name    string
    Pwd     string
    Host    string
    Subject string
    Body    string
    Send    string
}

func SendMail(user, password, host, to, subject, body, mailtype string) error {
    fmt.Println("Send to " + to)
    hp := strings.Split(host, ":")
    auth := smtp.PlainAuth("", user, password, hp[0])
    var content_type string
    if mailtype == "html" {
        content_type = "Content-Type: text/html;charset=UTF-8"
    } else {
        content_type = "Content-Type: text/plain;charset=UTF-8"
    }
    body = strings.TrimSpace(body)
    msg := []byte("To: " + to + "\r\nFrom: " + user + "<" + user + ">\r\nSubject: " + subject + "\r\n" + content_type + "\r\n\r\n" + body)
    send_to := strings.Split(to, ";")
    err := smtp.SendMail(host, auth, user, send_to, msg)
    if err != nil {
        fmt.Println(err.Error())
    }
    return err
}

func readLine2Array(filename string) ([]string, error) {
    result := make([]string, 0)
    file, err := os.Open(filename)
    if err != nil {
        return result, errors.New("Open file failed.")
    }
    defer file.Close()
    bf := bufio.NewReader(file)
    for {
        line, isPrefix, err1 := bf.ReadLine()
        if err1 != nil {
            if err1 != io.EOF {
                return result, errors.New("ReadLine no finish")
            }
            break
        }
        if isPrefix {
            return result, errors.New("Line is too long")
        }
        str := string(line)
        result = append(result, str)
    }
    return result, nil
}

func DelArrayVar(arr []string, str string) []string {
    str = strings.TrimSpace(str)
    for i, v := range arr {
        v = strings.TrimSpace(v)
        if v == str {
            if i == len(arr) {
                return arr[0 : i-1]
            }
            if i == 0 {
                return arr[1:len(arr)]
            }
            a1 := arr[0:i]
            a2 := arr[i+1 : len(arr)]
            return append(a1, a2...)
        }
    }
    return arr
}

func LoadData() {
    fmt.Println("LoadData")
    file, err := os.Open("data.dat")
    defer file.Close()
    if err != nil {
        fmt.Println(err.Error())
        SJ.Name = "用户名"
        SJ.Pwd = "用户密码"
        SJ.Host = "SMTP服务器:端口"
        SJ.Subject = "邮件主题"
        SJ.Body = "邮件内容"
        SJ.Send = "要发送的邮箱,每行一个"
        return
    }
    dec := gob.NewDecoder(file)
    err2 := dec.Decode(&SJ)
    if err2 != nil {
        fmt.Println(err2.Error())
        SJ.Name = "用户名"
        SJ.Pwd = "用户密码"
        SJ.Host = "SMTP服务器:端口"
        SJ.Subject = "邮件主题"
        SJ.Body = "邮件内容"
        SJ.Send = "要发送的邮箱,每行一个"
    }
}

func SaveData() {
    fmt.Println("SaveData")
    file, err := os.Create("data.dat")
    defer file.Close()
    if err != nil {
        fmt.Println(err)
    }
    enc := gob.NewEncoder(file)
    err2 := enc.Encode(SJ)
    if err2 != nil {
        fmt.Println(err2)
    }
}

var SJ ShuJu
var runing bool
var chEnd chan bool

func main() {
    LoadData()
    chEnd = make(chan bool)
    var emails, body, msgbox *walk.TextEdit
    var user, password, host, subject *walk.LineEdit
    var startBtn *walk.PushButton
    MainWindow{
        Title:   "邮件发送器",
        MinSize: Size{800, 600},
        Layout:  HBox{},
        Children: []Widget{
            TextEdit{AssignTo: &emails, Text: SJ.Send, ToolTipText: "待发送邮件列表,每行一个"},
            VSplitter{
                Children: []Widget{
                    LineEdit{AssignTo: &user, Text: SJ.Name, CueBanner: "请输入邮箱用户名"},
                    LineEdit{AssignTo: &password, Text: SJ.Pwd, PasswordMode: true, CueBanner: "请输入邮箱登录密码"},
                    LineEdit{AssignTo: &host, Text: SJ.Host, CueBanner: "SMTP服务器:端口"},
                    LineEdit{AssignTo: &subject, Text: SJ.Subject, CueBanner: "请输入邮件主题……"},
                    TextEdit{AssignTo: &body, Text: SJ.Body, ToolTipText: "请输入邮件内容", ColumnSpan: 2},
                    TextEdit{AssignTo: &msgbox, ReadOnly: true},
                    PushButton{
                        AssignTo: &startBtn,
                        Text:     "开始群发",
                        OnClicked: func() {
                            SJ.Name = user.Text()
                            SJ.Pwd = password.Text()
                            SJ.Host = host.Text()
                            SJ.Subject = subject.Text()
                            SJ.Body = body.Text()
                            SJ.Send = emails.Text()
                            SaveData()

                            if runing == false {
                                runing = true
                                startBtn.SetText("停止发送")
                                go sendThread(msgbox, emails)
                            } else {
                                runing = false
                                startBtn.SetText("开始群发")
                            }
                        },
                    },
                },
            },
        },
    }.Run()
}

func sendThread(msgbox, es *walk.TextEdit) {
    sendTo := strings.Split(SJ.Send, "\r\n")
    susscess := 0
    count := len(sendTo)
    for index, to := range sendTo {
        if runing == false {
            break
        }
        msgbox.SetText("发送到" + to + "..." + strconv.Itoa((index/count)*100) + "%")
        err := SendMail(SJ.Name, SJ.Pwd, SJ.Host, to, SJ.Subject, SJ.Body, "html")
        if err != nil {
            msgbox.AppendText("\r\n失败:" + err.Error() + "\r\n")
            if err.Error() == "550 Mailbox not found or access denied" {
                SJ.Send = strings.Join(DelArrayVar(strings.Split(SJ.Send, "\r\n"), to), "\r\n")
                es.SetText(SJ.Send)
            }
            time.Sleep(1 * time.Second)
            continue
        } else {
            susscess++
            msgbox.AppendText("\r\n发送成功!")
            SJ.Send = strings.Join(DelArrayVar(strings.Split(SJ.Send, "\r\n"), to), "\r\n")
            es.SetText(SJ.Send)
        }
        time.Sleep(1 * time.Second)
    }
    SaveData()
    msgbox.AppendText("停止发送!成功 " + strconv.Itoa(susscess) + " 条\r\n")
}

效果:
这里写图片描述


这里写图片描述

2
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

golang 开发gui,还真有gui的框架,就是做个简单的行

1,关于guigolang 还真的有gui的开发框架。在mac上面好像比较简单。直接用就好。 不知道在其他平台上面咋样。 github项目地址: https://github.com/andlabs/ui/wiki/Getting-Started 起重核心还是使用了:https://gith...
  • freewebsys
  • freewebsys
  • 2017-03-04 19:05
  • 6833

Golang GUI入门——andlabs ui

官方不提供gui标准库,只好寻求第三方库。 https://github.com/google/gxui 这个gui库是谷歌内部人员提供的,并不是谷歌官方出品,现在停止维护,只好作罢。 第三方gui库 找了好多,也比较了好多,最终决定使用的是还是 https://github.com/an...
  • u013474104
  • u013474104
  • 2016-12-06 14:28
  • 2888

本文主要讲述golang的gui库andlabs/ui使用

本文主要讲述golang的gui库andlabs/ui使用。 目前该库还不是很完善。 环境说明: 系统:Win10 64Go:1.7.5 (ui库规定需要>=1.6)注意: 不支持win Xp系统mingw64版本要5.0以上 下载安装MSYS2 下载地址:...
  • u013870094
  • u013870094
  • 2017-06-11 16:27
  • 492

goLang 如何开发 windows 窗口界面

今天找了一下。找到了一个 walk的一个东西。不用说下get一下这个pack下了再说 go get github.com/lxn/walkget下来后 访问了一下github 页面看了一下作者的说明 Walk是一个写给Golang的Window应用程序库套件,它主要用于桌面GUI的开发,但也有更...
  • liangguangchuan
  • liangguangchuan
  • 2016-05-30 10:14
  • 5230

Go语言GUI Demo 之 Walk

Go语言没有自带官方Gui,目前找到的Gui框架中感觉Walk还不错,但该库只支持Windows操作系统(一般也只用到Windows)。本次我把官方的example编译成exe,方便网友参考Walk 的gui功能。Walk地址:https://github.com/lxn/walk以下是各demo的...
  • sunansheng
  • sunansheng
  • 2016-11-10 15:35
  • 14005

使用GO开发桌面GUI程序

使用GO来开发桌面GUI程序,个人感觉有几个好处: 静态编译后只生成单个文件。实现小型和工具型程序不需依赖,易于分享。 直接编译为exe等可执行文件,不用像PyQt、Electron那样再打包。 跨平台编译能力,同时支持多个跨平台库。 并发优化,协程支持,开发多线程GUI程序,比Python效率更优...
  • github_38589282
  • github_38589282
  • 2017-11-09 21:29
  • 442

go的gui----walk的使用

go虽然是服务端语言,但是使用go也可以用于实现客户端,这里使用walk来实现。 walk的git地址:https://github.com/lxn/walk walk的说明文档:https://godoc.org/github.com/lxn/walk      ...
  • hangeqq685042
  • hangeqq685042
  • 2016-08-09 14:37
  • 1335

我的Go语言学习之旅七:创建一个GUI窗体

在上次中,刚刚学过了  弹窗效果,这里再接着学习一下如何创建一个窗体。还是老路子,先上代码:package mainimport ("github.com/lxn/go-winapi""syscall""strconv""...
  • w_yunlong
  • w_yunlong
  • 2015-12-29 11:57
  • 2791

Go实战--golang中读写文件的几种方式

生命不止,继续 go go go !!!读写文件应该是在开发过程中经常遇到的,今天要跟大家一起分享的就是在golang的世界中,如何读写文件。使用io/ioutil进行读写文件先回忆下之前的ioutil包介绍: Go语言学习之ioutil包(The way to go)其中提到了两个方法: fu...
  • wangshubo1989
  • wangshubo1989
  • 2017-07-07 19:49
  • 9462

golang 使用negroni,实现server

golang刚看不就,写个小程序练练手 ,借助negroni库实现服务器,客户端借助golang自身的http,实现了客户端与服务器交互
  • abqchina
  • abqchina
  • 2016-12-28 11:26
  • 1037
    个人资料
    • 访问:4551232次
    • 积分:40850
    • 等级:
    • 排名:第105名
    • 原创:566篇
    • 转载:29篇
    • 译文:13篇
    • 评论:768条
    微信公众号
      我的微信公众号
      为你推荐最新的博文~更有惊喜等着你
    时光荏苒
      白驹过隙
    博客专栏
    文章分类
    百度统计
    Google Analytics