【译】Go和WebAssembly:在浏览器中运行Go程序

在过去很长一段时间里,Javascript是Web开发人员中的通用语言。如果你想写一个稳定成熟的 Web 应用程序,用javascript几乎是唯一的方法。

WebAssembly(也称为wasm)将很快改变这种情况。使用WebAssembly可以用任何语言编写Web应用程序。在本文中,我们将了解如何编写Go程序并使用wasm在浏览器中运行它们。

但首先,什么是WebAssembly

webassembly.org 将其定义为“基于堆栈的虚拟机的二进制指令格式”。这是一个很好的定义,但让我们将其分解为我们可以轻松理解的内容。

从本质上讲,wasm是一种二进制格式; 就像ELF,Mach和PE一样。唯一的区别是它适用于虚拟编译目标,而不是实际的物理机器。为何虚拟?因为不同于 C/C++ 二进制文件,wasm二进制文件不针对特定平台。因此,您可以在Linux,Windows和Mac中使用相同的二进制文件而无需进行任何更改。 因此,我们需要另一个“代理”,它将二进制文件中的wasm指令转换为特定于平台的指令并运行它们。通常,这个“代理”是一个浏览器,但从理论上讲,它也可以是其他任何东西。

这为我们提供了一个通用的编译目标,可以使用我们选择的任何编程语言构建Web应用程序!只要我们编译为wasm格式,我们就不必担心目标平台。就像我们编写一个Web应用程序一样,但是现在我们有了用我们选择的任何语言编写它的优势。

你好 WASM

让我们从一个简单的“hello world”程序开始,但是要确保您的Go版本至少为1.11。我们可以这样写:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("hello wasm")
}

保存为test.go。看起来像是一个普通的Go程序。现在让我们将它编译为wasm平台程序。我们需要设置GOOSGOARCH

$GOOS=js GOARCH=wasm go build -o test.wasm test.go

现在我们生成了 wasm 二进制文件。但与原生系统不同,我们需要在浏览器中运行它。为此,还需要再做一点工作来实现这一目标:

  • Web服务器来运行应用
  • 一个index.html文件,其中包含加载wasm二进制文件所需的一些js代码。
  • 还有一个js文件,它作为浏览器和我们的wasm二进制文件之间的通信接口。

我喜欢把它想象成制作The PowerPuff Girls所需要的东西。

然后,BOOM,我们有了一个WebAssembly应用程序!

现在Go目录中已经包含了html和js文件,因此我们将其复制过来。

$cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
$cp "$(go env GOROOT)/misc/wasm/wasm_exec.html" .
$# we rename the html file to index.html for convenience.
$mv wasm_exec.html index.html
$ls -l
total 8960
-rw-r--r-- 1 agniva agniva    1258 Dec  6 12:16 index.html
-rwxrwxr-x 1 agniva agniva 6721905 Sep 24 12:28 serve
-rw-rw-r-- 1 agniva agniva      76 Dec  6 12:08 test.go
-rwxrwxr-x 1 agniva agniva 2425246 Dec  6 12:09 test.wasm
-rw-r--r-- 1 agniva agniva   11905 Dec  6 12:16 wasm_exec.js

serve是Go二进制文件,是一个Web服务器。但几乎任何Web服务器都可以。(译者注:原文并没有提供serve二进制文件的源代码,相信聪明的你一定知道怎样编写。)

一旦运行它,并打开浏览器。可以看到一个Run按钮,点击它,将执行我们的应用程序。然后我们点击它并检查控制台:

真牛,我们刚刚在Go中编写了一个程序并在浏览器中运行它。

到现在为止一切顺利。但这是一个简单的“hello world”程序。真实的Web应用程序需要与DOM交互。我们需要响应按钮单击事件,从文本框中获取输入数据,并将数据发送回DOM。现在我们将构建一个最小的图像编辑器,它将使用所有这些功能。

DOM API

但首先,要使Go代码与浏览器进行交互,我们需要一个DOM API。我们有syscall/js库来帮助我们解决这个问题。它是一个非常简单却功能强大的DOM API形式,我们可以在其上构建我们的应用程序。在我们制作应用程序之前,让我们快速了解它的一些功能。

回调

为了响应DOM事件,我们声明了回调并用这样的事件将它们连接起来:

import "syscall/js"

// Declare callback
cb := js.NewEventCallback(js.PreventDefault, func(ev js.Value) {
	// handle event
})


// Hook it up with a DOM event
js.Global().Get("document").
	Call("getElementById", "myBtn").
	Call("addEventListener", "click", cb)


// Call cb.Release() on your way out.

更新DOM

要从Go中更新DOM,我们可以

import "syscall/js"

js.Global().Get("document").
		Call("getElementById", "myTextBox").
		Set("value", "hello wasm")

您甚至可以调用JS函数并操作本机JS对象,如 FileReaderCanvas。查看syscall/js文档以获取更多详细信息。

正确的 Web 应用程序

接下来我们将构建一个小应用程序,它将获取输入的图像,然后对图像执行一些操作,如亮度,对比度,色调,饱和度,最后将输出图像发送回浏览器。 每个效果都会有滑块,用户可以更改这些效果并实时查看目标图像的变化。

首先,我们需要从浏览器获取输入的图像给到我们的Go代码,以便可以处理它。为了有效地做到这一点,我们需要采取一些不安全的技巧,这里跳过具体细节。拥有图像后,它完全在我们的控制之下,我们可以自由地做任何事情。下面是图像加载器回调的简短片段,为简洁起见略有简化:

onImgLoadCb = js.NewCallback(func(args []js.Value) {
	reader := bytes.NewReader(inBuf) // inBuf is a []uint8 slice where our image is loaded
	sourceImg, _, err := image.Decode(reader)
	if err != nil {
		// handle error
	}
	// Now the sourceImg is an image.Image with which we are free to do anything!
})

js.Global().Set("loadImage", onImgLoadCb)

然后我们从效果滑块中获取用户值,并操纵图像。我们使用了很棒的bild库。下面是回调的一小部分:

import "github.com/anthonynsimon/bild/adjust"

contrastCb = js.NewEventCallback(js.PreventDefault, func(ev js.Value) {
	delta := ev.Get("target").Get("valueAsNumber").Float()
	res := adjust.Contrast(sourceImg, delta)
})

js.Global().Get("document").
		Call("getElementById", "contrast").
		Call("addEventListener", "change", contrastCb)

在此之后,我们将目标图像编码为jpeg并将其发送回浏览器。这是完整的应用程序:

加载图片:

改变对比:

改变色调:

太棒了,我们可以在浏览器中本地操作图像而无需编写一行Javascript! 源代码可以在这里找到。

请注意,所有这些都是在浏览器本身中完成的。这里没有Flash插件,Java Applet或Silverlight。而是使用浏览器本身支持的开箱即用的WebAssembly。

最后的话

我的一些结束语:

  • 由于Go是一种垃圾收集语言,因此整个运行时都在wasm二进制文件中。因此,二进制文件通常有几MB的大小。与C/Rust等其他语言相比,这仍然是一个痛点; 因为向浏览器发送MB级数据并不理想。但是,如果wasm规范本身支持GC,那么这可能会改变。

  • Go中的Wasm支持正式进行试验。syscall/js API本身也在不断变化,未来可能会发生变化。如果您发现错误,请随时在我们issues报告问题。

  • 与所有技术一样,WebAssembly也不是一颗银弹。有时,简单的JS更快更容易编写。然而,wasm规范本身正在开发中,并且即将推出更多功能。线程支持就是这样一个特性。

希望这篇文章展示了WebAssembly的一些很酷的方面,以及如何使用Go编写功能齐全的Web应用程序。如果您发现错误,请尝试一下,并提出问题。如果您需要任何帮助,请随时访问 #webassembly频道。

原文链接

Go and WebAssembly: running Go programs in your browser

腾讯防水墙验证码使用

前阵子腾讯出了个验证码产品,宣称“告别传统验证码的单点防御,十道安全栅栏打造立体全面的安全验证,将黑产拒之门外”。看起来很不错的样子,正好之前使用过另外一家类似的产品,但是当时没有试用,今天特地注册了解了一下。

注册很简单,填写手机号邮箱和域名等即可。由于是在本地环境测试,似乎填写的域名并没有什么影响。而且填写时可以选择适用场景,应该是针对不同的场景有不同的策略。

功能也比较丰富,支持:

  • 2000次/小时安全防护

  • 支持免验证+分级验证

  • 三分钟快速接入

  • 全功能配置后台

  • 支持HTTPS

  • 阈值内流量无广告

注册完之后会分配一个 appidApp Secret KeyApp Secret Key 需要妥善保存,不可暴露出来。

下面就简单的记录下普通场景下如何使用。(此场景指简单使用,非验证码配置的场景)

0.前端页面

在 HTML 中引入js文件:

<script src="https://ssl.captcha.qq.com/TCaptcha.js"></script>

然后在需要激活的位置加入:

<button id="TencentCaptcha"
  data-appid="200700xxxx"
  data-cbfn="callback"
>验证</button>

官方文档表示可以使用其他标签,只需有 idcbfn 属性即可。

然后注册回调函数:

window.callback = function(res){
  console.log(res)
  // res(未通过验证)= {ret: 1, ticket: null}
  // res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
  if(res.ret === 0){
      alert(res.ticket)   // 票据
  }
}

前端的 callback 如果验证成功后,就可以在提交信息的同时把腾讯返回的内容提交给后端,主要是验证票据:ticket 和随机字符串:randstr

我测试的例子中是这样的:

<form action="/verify" method="post">
  <input type="text" name="appid" value="200700xxxx"/>
  <input type="text" id="ticket" name="ticket"/>
  <input type="text" id="randstr" name="randstr"/>
  <button id="btn" disabled>submit</button>
</form>

修改回调函数:

window.callback = function(res){
  console.log(res)
  if(res.ret === 0){
    document.getElementById('ticket').value = res.ticket
    document.getElementById('randstr').value = res.randstr
    document.getElementById('ticket').value = res.ticket
    document.getElementById('btn').disabled = false
  } else {
    alert('验证失败')
  }
}

前端验证成功后,把 ticketrandstr 填充到表单中去。

此时页面就可以使用这个验证服务了。

1.后端

后端拿到提交的表单后,需要再去请求腾讯的接口验证是否成功。

如下:

func serveVerify(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Method not allowed", 405)
        return
    }

    r.ParseForm()

    aid := r.Form["appid"][0]
    AppSecretKey := "yourSecretKey"
    UserIP := r.RemoteAddr
    Ticket :=  r.Form["ticket"][0]
    Randstr := r.Form["randstr"][0]

    req, err := http.NewRequest("GET", API, nil)
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    q := req.URL.Query()
    q.Add("aid", aid)
    q.Add("AppSecretKey", AppSecretKey)
    q.Add("UserIP", UserIP)
    q.Add("Ticket", Ticket)
    q.Add("Randstr", Randstr)
    req.URL.RawQuery = q.Encode()

    httpClient := &http.Client{
        Timeout: 10*time.Second,
    }

    fmt.Println("going to check :",req.URL.String())

    resp, err := httpClient.Do(req)
    defer resp.Body.Close()
    if err != nil {
        w.Write([]byte("got error"))
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        w.Write([]byte("got error"))
    }

    w.Write(body)
}

腾讯服务器将返回 {response:1, evil_level:70, err_msg:""} 类似的内容。其中:

response = 1 表示验证成功。

evil_level 是恶意等级,范围为0-100。

response = 1 即表示此次请求是“正常”的。

总结

确实很容易接入,而且还提供了验证数据的请求统计等,包括通过与拦截的数据,日请求量,通过量,拦截量。

后台还提供了对场景更改的操作,也可以定制外观。

整体还不错。

Git 同步上游源更改

在 Git 上 fork 了一个项目后,如果没有经常合并上游的更改,或者 fork 后的项目提交代码后没有提交到上游,就会出现

This branch is X commits ahead, Y commits behind”

可以通过如下的方式同步上游项目。

在本地添加上游项目:

git remote add upstream git@github:upstream/repo.git

当需要同步时,执行:

git pull --rebase upstream master
git push --force-with-lease origin master

–rebase 和 –force-with-lease 选项在没有合并到上游提交时才有必要。

以下还有几个可以用到的命令:

重置本地的更改到上游的状态:

git reset --hard upstream/master

其实通常使用中,建议为 功能/错误修复 创建一个新的分支。这样可以在等待 PR 被合并时,开始另一个 功能/错误修复 的开发。如果从不直接提交给 master,那么可以不用使用 –rebase 或 –force-with-lease 进行同步:

git checkout master
git pull upstream master
git push origin master

在更新主分支后更新功能分支:

git checkout myfeature
git rebase master
git push --force-with-lease origin myfeature

【译】使用 Go,Echo 和 Vue 创建单页 TODO 应用

本教程中我们将会创建一个 “todo” 应用。完成后可以实现创建任务,展示新创建的任务和删除它们。

此程序后端使用 Go 语言。Go 由 Google 开发。虽然不是最流行的语言,但是正在逐步得到认可。Go 非常轻量级,易于学习,运行快。此教程假设你已经对于这门语言有了一些了解,并且已经安装和配置好了开发环境。

我们将会使用 Echo 框架,Echo 框架相当于 PHP 语言的 Slim PHP 或 Lumen 框架,你应该有点熟悉使用微框架和使用路由处理 http 请求的概念。

任务数据将会储存在 SQLite 数据库中,SQLite 是一个轻量级的可替代 MySQL 或 PostgreSQL 的数据库。数据会存在一个独立的文件中,与应用在同一目录而不是存在服务器上。

最后,前端使用 HTML5 和流行的 VueJS JavaScript 框架,需要对 VueJS 有一定的了解。

我们的应用程序将分解成四个基本部分。我们将拥有我们的主包,用来设置路由和数据库。接下来,我们将有几个处理程序用来处理不同的路由。 当然,我们也会有一个 Task 模型,它将使用 SQLite 进行持久化。最后,应用程序将有一个简单的 index.html 文件,其中包含我们的 HTML5 和 VueJS 客户端代码。 让我们深入挖掘!

路由和数据库

在入口文件中会引入几个包。”database/sql” 是Go标准包,但是 Echo 和 SQLite 需要从 Github 下载。

$ go get github.com/labstack/echo
$ go get github.com/mattn/go-sqlite3

然后创建应用的目录。

$ cd $GOPATH/src
$ mkdir go-echo-vue && cd go-echo-vue

现在开始写路由,在 go-echo-vue 目录创建一个文件并命名为 “todo.go” ,然后引入 Echo 框架。

// todo.go
package main

import (
	"github.com/labstack/echo"
	"github.com/labstack/echo/engine/standard"
)

下一步,创建go程序必需的 “main” 方法。

// todo.go
func main() { }

为了使前端的 VueJS 可以和后端通信,创建任务,需要设置一些基本的路由。第一件事就是实例化一个 Echo 。然后使用內建方法定义几个路由。如果使用过其他框架,应该会熟悉这个概念。

路由使用一个正则作为第一个参数,然后使用一个处理方法作为第二个参数。在此教程中必须使用 Echo.HandlerFunc 接口。

现在可以在 “main” 方法中创建几个给前端通信使用的路由了。

// todo.go
func main() {
    // Create a new instance of Echo
    e := echo.New()

    e.GET("/tasks", func(c echo.Context) error { return c.JSON(200, "GET Tasks") })
    e.PUT("/tasks", func(c echo.Context) error { return c.JSON(200, "PUT Tasks") })
    e.DELETE("/tasks/:id", func(c echo.Context) error { return c.JSON(200, "DELETE Task "+c.Param("id")) })

    // Start as a web server
    e.Run(standard.New(":8000"))
}

以上路由只输出了固定的文本内容,将会在接下来改进。

可以使用 Postman 测试以上接口。

$ go build todo.go
$ ./todo

运行后,打开 Postman,输入 localhost:8000,选择 GET 来测试 “/tasks” 路由,正常可以看到 “GET Tasks”。

然后是配置数据库,指定存储文件为 “storage.db” ,如果不存在程序会自动创建。数据库创建后需要运行数据迁移。

// todo.go

import (
	"database/sql"

	"github.com/labstack/echo"
	"github.com/labstack/echo/engine/standard"
	_ "github.com/mattn/go-sqlite3"
)

在 main 方法里增加

// todo.go
func main() {
	db := initDB("storage.db")
	migrate(db)

然后需要定义 initDB 和 migrate 方法。

// todo.go
func initDB(filepath string) *sql.DB {
	db, err := sql.Open("sqlite3", filepath)

	//检查错误
	if err != nil {
		panic(err)
	}

	//如果open没有报错,但是仍然没有数据库连接,一样要退出
	if db == nil {
		panic("db nil")
	}

	return db
}

func migrate(db *sql.DB) {
	sql := `
	CREATE TABLE IF NOT EXISTS tasks(
		id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
		name VARCHAR NOT NULL
	);
	`

	_, err := db.Exec(sql)

	//出错退出
	if err != nil {
		panic(err)
	}
}

这两个方法用于连接数据库,建表。”initDB” 会打开一个 db 文件或者创建它。如果失败程序会退出。

“migrate” 方法运行创建表的 SQL 。如果失败程序退出。

然后

$ go build todo.go
$ ./todo

查看效果。

如果打开另一个终端,列出当前目录内容时会发现已经创建了 “storage.db” ,执行以下命令来确认它确实是个 SQLite 文件。

$ sqlite3 storage.db

需要安装 SQLite 才可以执行此命令。

此命令会给出提示,输入 “.tables” ,可以列出所有的表,输入 “.quit” 退出。

处理请求

之前已经创建了与前端交互的接口,现在需要创建或删除任务时给客户端真实的结果。这需要几个方法去完成。

在 “todo.go” 中需要引入新的包。

package main
import (
    "database/sql"
    "go-echo-vue/handlers"

    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
    _ "github.com/mattn/go-sqlite3"
)

然后修改路由,使用刚刚创建的 handlers 包去处理。

// todo.go
    e := echo.New()

    e.File("/", "public/index.html")
    e.GET("/tasks", handlers.GetTasks(db))
    e.PUT("/tasks", handlers.PutTask(db))
    e.DELETE("/tasks/:id", handlers.DeleteTask(db))

    e.Run(standard.New(":8000"))
}

查看这段代码,你可能会注意到列出的处理程序实际上并不遵循 Echo 所要求的函数签名。相反,这些函数返回一个满足该接口的函数。这是我用过的一个技巧,所以我们可以将 db 实例从 handler 传递到 handler ,而不必在每次我们要使用数据库时创建一个新实例。稍后会更清楚。

我们还增加了一条额外的路由。这是一个包含我们的 VueJS 客户端代码的静态 html 文件。我们可以使用 “File” 功能提供静态文件。在这种情况下,将在访问 “/“ 的时候输出我们的客户端代码。

然后创建一个名为 “handlers” 的目录,并在该目录中创建一个名为 “tasks.go” 的文件。接下来,我们需要导入一些我们需要的软件包。

// handlers/tasks.go
package handlers

import (
    "database/sql"
    "net/http"
    "strconv"

    "github.com/labstack/echo"
)

接下来的这一行代码,它允许我们在响应中返回任意的 JSON ,就像你稍后会看到的一样。这是一个以字符串作为 key ,任意类型作为值的 map 结构。 在Go中,”interface” 关键字表示从原始数据类型到用户定义类型或结构的任何内容。

// hanlers/tasks.go
type H map[string]interface{}

这个文件主要是处理函数。它们都以 db 连接作为参数,但要记住, Echo 路由的正确处理程序,需要实现 Echo.HandlerFunc 接口。 我们通过返回与接口签名匹配的匿名函数来实现此目标。该函数现在可以使用数据库连接并将其传递给我们的模型。

为了能正常工作,暂时我们不会处理数据库。只会返回一些假数据。

// handlers/tasks.go

// GetTasks endpoint
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusOK, "tasks")
    }
}

// PutTask endpoint
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusCreated, H{
            "created": 123,
    }
}

// DeleteTask endpoint
func DeleteTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        return c.JSON(http.StatusOK, H{
            "deleted": id,
        })
    }
}

Go http 软件包为我们提供了一些便利的常量来表示 HTTP 状态代码。例如,我们使用 http.StatusCreated 作为我们的 PUT 响应。 这些处理程序中的每一个现在都会返回 JSON 格式的响应。最后一个函数 “DeleteTask” 需要一个 id 参数。我们使用 strconv 包和 Atoi(alpha to integer)函数来确保 id 被转换为整数。 保证在通过数据库中的 id 查询任务时正确使用它。

要测试这些处理程序,要重新编译并运行应用程序。我们可以使用 Postman 再次测试。

MODEL

现在我们已经有了一部分处理程序,我们的应用程序需要使用数据库。但是我们不是直接从处理程序进行数据库调用,而是通过将数据库逻辑抽象为模型来保持代码的整洁。

首先让我们在新创建的处理程序文件中引用我们的新模型。

导入我们即将创建的模型包。

// handlers/tasks.go
package handlers

import (
    "database/sql"
    "net/http"
    "strconv"

    "go-echo-vue/models"

    "github.com/labstack/echo"
)

然后将调用添加到我们的处理函数中。

// handlers/tasks.go

// GetTasks endpoint
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // Fetch tasks using our new model
        return c.JSON(http.StatusOK, models.GetTasks(db))
    }
}

// PutTask endpoint
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // Instantiate a new task
        var task models.Task
        // Map imcoming JSON body to the new Task
        c.Bind(&task)
        // Add a task using our new model
        id, err := models.PutTask(db, task.Name)
        // Return a JSON response if successful
        if err == nil {
            return c.JSON(http.StatusCreated, H{
                "created": id,
            })
        // Handle any errors
        } else {
            return err
        }
    }
}

// DeleteTask endpoint
func DeleteTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        // Use our new model to delete a task
        _, err := models.DeleteTask(db, id)
        // Return a JSON response on success
        if err == nil {
            return c.JSON(http.StatusOK, H{
                "deleted": id,
            })
        // Handle errors
        } else {
            return err
        }
    }
}

现在在 “PutTask” 函数中,你会看到 “c.Bind”。 将会在 PUT 请求中发送 JSON 格式的响应内容,并将其映射到 Task 结构。 Task 结构将在我们的模型包中定义。

这里还需要注意一些错误检查。 Tasks Model 具有根据操作是否成功返回数据或错误的函数。我们的处理程序需要做出相应的处理。

现在我们可以创建我们的模型。 这是实际与数据库进行交互的。 创建一个名为 “models” 的目录,并在该目录中创建一个名为 “tasks.go” 的文件。

然后引入需要的包。

// models/tasks.go
package models

import (
    "database/sql"

    _ "github.com/mattn/go-sqlite3"
)

接下来,我们需要创建一个 Task 类型,包含两个字段 ID 和 Name。 Go 允许使用反引号将元数据添加到变量。在这种情况下,我们只是定义了每个字段在转换为 JSON 后的样子。 “c.Bind” 函数在填充新 Task 时知道在如何映射 JSON 数据。

另外还需要一个表示 Task 的集合的模型。

// models/tasks.go

// Task is a struct containing Task data
type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// TaskCollection is collection of Tasks
type TaskCollection struct {
    Tasks []Task `json:"items"`
}

获取任务只需从数据库中查询所有 Task,将其放到 TaskCollection 并返回。

// models/tasks.go

func GetTasks(db *sql.DB) TaskCollection {
    sql := "SELECT * FROM tasks"
    rows, err := db.Query(sql)
    // Exit if the SQL doesn't work for some reason
    if err != nil {
        panic(err)
    }
    // make sure to cleanup when the program exits
    defer rows.Close()

    result := TaskCollection{}
    for rows.Next() {
        task := Task{}
        err2 := rows.Scan(&task.ID, &task.Name)
        // Exit if we get an error
        if err2 != nil {
            panic(err2)
        }
        result.Tasks = append(result.Tasks, task)
    }
    return result
}

PutTask 将新 Task 插入数据库,并在成功时返回新的ID,并在失败时 panic。

// models/tasks.go

func PutTask(db *sql.DB, name string) (int64, error) {
    sql := "INSERT INTO tasks(name) VALUES(?)"

    // Create a prepared SQL statement
    stmt, err := db.Prepare(sql)
    // Exit if we get an error
    if err != nil {
        panic(err)
    }
    // Make sure to cleanup after the program exits
    defer stmt.Close()

    // Replace the '?' in our prepared statement with 'name'
    result, err2 := stmt.Exec(name)
    // Exit if we get an error
    if err2 != nil {
        panic(err2)
    }

    return result.LastInsertId()
}

DeleteTask 用来删除 Task。

// models/tasks.go

func DeleteTask(db *sql.DB, id int) (int64, error) {
    sql := "DELETE FROM tasks WHERE id = ?"

    // Create a prepared SQL statement
    stmt, err := db.Prepare(sql)
    // Exit if we get an error
    if err != nil {
        panic(err)
    }

    // Replace the '?' in our prepared statement with 'id'
    result, err2 := stmt.Exec(id)
    // Exit if we get an error
    if err2 != nil {
        panic(err2)
    }

    return result.RowsAffected()
}

请注意,我们通过 “db.Prepare” 在我们的模型函数中使用准备好的 SQL 语句。有两个原因。首先,一个准备好的语句可以被编译和缓存,所以执行多次更快。 其次,最重要的是准备好的语句可以防止 SQL 注入攻击。

现在再次使用 Postman 。 首先,我们将检查 “GET /tasks “ 。正常应该看到 Tasks 为空的 JSON。

现在来添加一个 Task 。 在 Postman 中,将 HTTP请求方式切换到 “PUT”,然后单击 “Body” 选项卡。 选中 “raw” 并选择 JSON(application/json) 作为类型。 在文本框中输入以下内容。

{
    "name": "Foobar"
}

提交后应该收到 “created” 的响应。

记下返回的 id ,因为我们需要它来测试 “DELETE /tasks” 。 就像在前面的例子中一样,将请求方式设置为 “DELETE” 并将 URL 改为 “/tasks/:id” 。 在我们以前的测试中用 “id” 替换 “:id”。 你应该得到一个成功的 “deleted” 消息。

现在可以再次请求 “GET /tasks”,正常应该返回 “null”。

前端

现在来处理我们的前端页面。为了简单起见,将我们的 Javascript 代码写在 HTML 中。标记很简单。 我们需要使用一些库,如 Bootstrap,JQuery,当然还有 VueJS。 用户界面只是一个输入框,一些按钮和任务的列表。 创建一个名为 ‘public’ 的目录,并在该目录内创建一个名为 “index.html” 的文件。

<!-- public/index.html -->

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">

        <title>TODO App</title>

        <!-- Latest compiled and minified CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

        <!-- Font Awesome -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">

        <!-- JQuery -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

        <!-- Latest compiled and minified JavaScript -->
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>

        <!-- Vue.js -->
        <script src="http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.24/vue.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.7.0/vue-resource.min.js"></script>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-md-4">
                    <h2>My Tasks</h2>
                    <ul class="list-group">
                        <li class="list-group-item" v-for="task in tasks">
                            {{ task.name }}
                            <span class="pull-right">
                                <button class="btn btn-xs btn-danger" v-on:click="deleteTask($index)">
                                    <i class="fa fa-trash-o" aria-hidden="true"></i>
                                </button>
                            </span>
                        </li>
                    </ul>
                    <div class="input-group">
                        <input type="text" class="form-control" placeholder="New Task" v-on:keyup.enter="createTask" v-model="newTask.name">
                        <span class="input-group-btn">
                            <button class="btn btn-primary" type="button" v-on:click="createTask">Create</button>
                        </span>
                    </div><!-- /input-group -->
                </div>
            </div>
        </div>
    </body>
</html>

现在重新运行我们的应用程序,然后打开 “http://localhost:8000“。

在最后的 “div” 标签下,将我们的 VueJS 代码放在一个 “script” 标签中。 VueJS 代码稍微复杂一点,但也非常明显。 我们有几个创建和删除任务的方法以及一个在初始化时运行的方法。初始化时获取当前存储在数据库中的所有任务。

为了与后端进行通信,我们需要使用 HTTP 客户端。在这种情况下,我们将使用 vue-resource。 可以通过引用 “this.$http” 然后使用任何 HTTP 请求方式(get,put等)来使用它。

<!-- public/index.html -->

       <script>
           new Vue({
               el: 'body',

               data: {
                   tasks: [],
                   newTask: {}
               },

         // This is run whenever the page is loaded to make sure we have a current task list
               created: function() {
       // Use the vue-resource $http client to fetch data from the /tasks route
                   this.$http.get('/tasks').then(function(response) {
                       this.tasks = response.data.items ? response.data.items : []
                   })
               },

               methods: {
                   createTask: function() {
                       if (!$.trim(this.newTask.name)) {
                           this.newTask = {}
                           return
                       }

            // Post the new task to the /tasks route using the $http client
                       this.$http.put('/tasks', this.newTask).success(function(response) {
                           this.newTask.id = response.created
                           this.tasks.push(this.newTask)
                           console.log("Task created!")
                           console.log(this.newTask)
                           this.newTask = {}
                       }).error(function(error) {
                           console.log(error)
                       });
                   },

                   deleteTask: function(index) {
            // Use the $http client to delete a task by its id
                       this.$http.delete('/tasks/' + this.tasks[index].id).success(function(response) {
                           this.tasks.splice(index, 1)
                           console.log("Task deleted!")
                       }).error(function(error) {
                           console.log(error)
                       })
                   }
               }
           })
       </script>

运行

现在我们的应用程序已经完成了。我们需要编译然后运行它。

$ go build todo.go
$ ./todo

然后打开 “http://localhost:8000“。

总结

在本教程中,我们学习了如何使用 Echo 框架和 VueJS 创建前端页面和简单的 Go 后端应用。 希望这会激起你对 Go 语言的好奇心,并激励你建立更复杂的 Web 应用。

原文链接

Create a Single Page App With Go, Echo and Vue

Keygen 包简介

有些时候,我们的业务需要生成随机字符串,数字甚至字节,例如:用户ID,API密钥,验证令牌等。自己写生成算法的话比较麻烦,今天正好看到 Keygen 包可以轻松完成这些。

简单记录一些用法。

安装 Keygen

使用 composer 安装:

composer require gladcodes/keygen

生成数字

数字通常用来作为 ID。可以通过调用 Keygen\Keygen 类的 numeric() 方法生成。它带有一个可选的长度参数,用来指定数字的长度,如果省略或者格式错误,则默认为16。

<?php

require __DIR__ . '/vendor/autoload.php';
use Keygen\Keygen;

$id_12 = Keygen::numeric(12)->generate();
$id_16 = Keygen::numeric()->generate();

echo $id_12; // 011683218639
echo $id_16; // 9352941287643963

通常情况下,我们不会使用零开头的数字。需要对代码进行细微的修改,以确保在数字的开头没有零。 以下代码片段创建一个自定义函数来包装生成机制。

<?php

require __DIR__ . '/vendor/autoload.php';
use Keygen\Keygen;

function generateID($length = null) {
    $length = is_int($length) ? $length : null;

    return Keygen::numeric($length - 1)->prefix(mt_rand(1, 9))->generate(true);
}

$id_12 = generateID(12);
$id_16 = generateID();

echo $id_12; // 473840499215
echo $id_16; // 2684603281019122

上面的代码使用 prefix() 方法在数字的开头添加一个非零整数。Keygen 软件包还提供了一个 suffix() 方法,用于在生成的密钥末尾添加字符。 有关 Keygen 软件包功能的更多详细信息,例如:Key Affixes 和 Key Transformations,请参阅 Keygen 软件包的 README 文档。

生成字符串和 Token

字符串是包含大写字母,小写字母和数字组合的随机字符序列。它可以通过静态调用 Keygen\Keygen 类的 alphanum() 方法生成,其方法与 numeric() 方法非常相似。

Token 是随机的 base64 编码的字符串。它通常用作应用程序的秘密和 API 密钥。它可以由 Keygen\Keygen 类的 token() 方法生成。

<?php

require __DIR__ . '/vendor/autoload.php';
use Keygen\Keygen;

$alnum = Keygen::alphanum(15)->generate();
$token = Keygen::token(28)->generate();

echo $alnum; // TFd5X74Pr9ZOiG2
echo $token; // 4HE1xQz+4ks0Td128KSO/kBivd79

生成随机字节

还通过调用 Keygen\Keygen 类的 bytes() 方法来生成随机字节。

一般情况下随机字节不是很有用,所以,Keygen 包提供了十六进制(hex)的随机字节的 hex() 方法。

<?php

require __DIR__ . '/vendor/autoload.php';
use Keygen\Keygen;

$bytes = Keygen::bytes(20)->generate();
$hex = Keygen::bytes(20)->hex()->generate();

echo $bytes; // 
echo $hex; // 9f802a80aaf4b5e89e14

值类型转换

Keygen 软件包允许在生成密钥之前对密钥做一次或多次转换。转换只是一种可调用的方法,可以将生成的键作为第一个参数并返回一个字符串。 每次转换在生成的密钥上按照它们在返回密钥前指定的顺序执行。

<?php

require __DIR__ . '/vendor/autoload.php';
use Keygen\Keygen;

$reference = Keygen::numeric(20)->generate(function($key) {
    return join('-', str_split($key, 4));
});

$filename = Keygen::bytes()->suffix('.png')->generate(true, ['strrev', function($key) {
    return substr(md5($key), mt_rand(0,8), 20);
}], 'strtoupper');

echo $reference; // 2129-1489-0676-5152-9337
echo $filename; // 159D702E346F74E3F0B6.png

Git rebase 笔记

Git 合并代码有 merge和 rebase 两种选择,个人观点是,merge 一般用来合并两个分支,而 rebase 一般用来合并 commit。

最近在给 mattermost 项目提交 PR 的时候被要求 rebase ,操作了几次都不成功,后来仔细看了文档才正确合并,所以又了以下记录。

如果有4次提交,hash分别是

commit1 ---> be8ad5c
commit2 ---> 57939ce
commit3 ---> 64be23a
commit4 ---> e0788e4

如果想把commit4合并到commit3,

git rebase -i 57939ce


pick 64be23a commit 3
pick e0788e4 commit 4

下方会有操作的注释

# s, squash = use commit, but meld into previous commit

对于想要合并的commit,使用s操作, 即

pick 64be23a commit 3
s e0788e4 commit 4

保存后,会进入交互编辑模式

# This is a combination of 2 commits.
# This is the 1st commit message:

commit 3

# This is the commit message #2:

commit 4

表示将要合并两个commit,可以修改或不修改commit内容,保存后,两次commit即合并成一个commit了。

这样,在 git 的提交历史里便没有了 commit 4 这次提交。

参考资料:

Git 分支 - 变基

Go 递归

递归,就是在运行的过程中调用自己。

通过以下阶乘函数来看下递归的写法:

func factorial(x uint) uint {
  if x == 0 {
    return 1
  }
  return x * factorial(x-1)
}

factorial 函数调用自己,形成函数递归,为了更好地理解这个函数是如何工作的,可以通过 factorial(2) 来理解。

  • x == 0 ? 不等于, x = 2

  • 计算 x - 1 的阶乘

    • x == 0 ? 不等于, x = 1

    • 计算 x - 1 的阶乘

      • x == 0 ? 等于, 返回 1
    • 返回 1 * 1

  • 返回 2 * 1

递归函数通过不断的调用自身完成需求,需要注意的是需要设置退出条件,否则就死循环了。

递归函数可以非常方便的解决数学上的问题,如阶乘,斐波那契数列等。

Go 闭包

在 Go 中可以在一个函数里创建另一个函数。如下:

func main() {
	add := func(x, y int) int {
		return x + y
	}

	fmt.Println(add(1,1))
}

add 是一个属性为 func(int, int) int (两个 int 类型参数,返回值类型为 int 的函数)的局部变量。这样的局部函数还可以访问其他局部变量。

func main() {
	x := 0
	increment := func() int {
		x++
		return x
	}

	fmt.Println(increment())
	fmt.Println(increment())
}

increment 函数为在 main 函数作用域中定义的变量 x 加1。 变量 x 可以被 increment 函数访问和修改。所以以上程序第一行将会输出:1,第二行将会输出:2。

这样的函数以及它引用的非本地变量称为闭包。 在本示例中,increment 函数和变量 x 形成闭包。

使用闭包的一种方法是编写一个函数,该函数返回另一个函数 - 当被调用时 - 可以生成一个数字序列。 例如,我们可以生成所有的偶数:

func makeEvenGenerator() func() uint {
	i := uint(0)
	return func() (ret uint) {
		ret = i
		i += 2
		return
	}
}

func main() {
	nextEven := makeEvenGenerator()
	fmt.Println(nextEven()) // 0
	fmt.Println(nextEven()) // 2
	fmt.Println(nextEven()) // 4
}

makeEvenGenerator 返回一个生成偶数的函数。 每次调用它时,它会将2添加到本地i变量中 - 与正常的局部变量不同,它会在调用之间保持不变。

原文

Closure

Dockerfile 构建参数

使用 Docker 做服务部署的时候,经常需要在构建的时候区分环境,让程序能够拿到环境变量,或者让程序能够针对不同环境做出不同的处理。

之前的写法比较原始,在本地打包 Docker 镜像后 push 到服务器,所以就可以在打包的时候修改环境变量的值。虽然只用到了一个区分开发,生产的变量,但是每次都这么做还是比较烦。

最近重新整理 Dockerfile , 又看了下文档,发现可以在 docker build 阶段传入参数的。

如下所示:

FROM golang

ARG app_env

ENV APP_ENV $app_env

...

build 时,可以这样写

docker build -t app -f Dockerfile . --build-arg app_env=dev

这样就可以在 go 程序里 通过 os.Getenv(“APP_ENV”) 拿到环境变量信息,进行不同处理了。

参考资料:

ARG 构建参数

Golang and Docker for development and production

迁移到 GitHub pages 小记

在 Vultr vps 挂了N个月之后,无奈只能选择把这堆东西迁到 GitHub pages 了,看起来是唯一能选的比较不错的选择。虽然很早以前也看过一些迁到 GitHub pages 的教程,但是实施起来还是有些新的收获。

开始前没注意看文档,其实创建 repo 的时候对于 usrename 的项目,repo 名必须是 username.github.io。

如果不想使用 username.github.io 作为域名的话,要在 repo->settings->GitHub Pages 中,设置一个 Custom domain,然后 Save。

GitHub 默认是提供了使用 jekyll 作为 GitHub pages 的内容处理程序。而如果并不想使用 jekyll 的话,也可以。比如我就是使用 hexo ,把 generate 的内容作为 git 内容提交了的。

但是还有一个要注意的是,把静态内容提交到 repo 后,需要过一会才会生效。所以刚提交完时,会返回 404 ,不要着急,过一会就好了。

另外目前 GitHub 还提供了使用自定义域名时启用 HTTPS 的选项,启用后就会强制使用 HTTPS 打开网站了。