08-rustlings-enums

enums1.rs

原始代码:

#[derive(Debug)]
enum Message {
    // TODO: Define a few types of messages as used below.
}

fn main() {
    println!("{:?}", Message::Resize);
    println!("{:?}", Message::Move);
    println!("{:?}", Message::Echo);
    println!("{:?}", Message::ChangeColor);
    println!("{:?}", Message::Quit);
}

编译报错内容:

error[E0599]: no variant or associated item named `Resize` found for enum `Message` in the current scope
 --> exercises/08_enums/enums1.rs:7:31
  |
2 | enum Message {
  | ------------ variant or associated item `Resize` not found for this enum
...
7 |     println!("{:?}", Message::Resize);
  |                               ^^^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Move` found for enum `Message` in the current scope
 --> exercises/08_enums/enums1.rs:8:31
  |
2 | enum Message {
  | ------------ variant or associated item `Move` not found for this enum
...
8 |     println!("{:?}", Message::Move);
  |                               ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Echo` found for enum `Message` in the current scope
 --> exercises/08_enums/enums1.rs:9:31
  |
2 | enum Message {
  | ------------ variant or associated item `Echo` not found for this enum
...
9 |     println!("{:?}", Message::Echo);
  |                               ^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `ChangeColor` found for enum `Message` in the current scope
  --> exercises/08_enums/enums1.rs:10:31
   |
2  | enum Message {
   | ------------ variant or associated item `ChangeColor` not found for this enum
...
10 |     println!("{:?}", Message::ChangeColor);
   |                               ^^^^^^^^^^^ variant or associated item not found in `Message`

error[E0599]: no variant or associated item named `Quit` found for enum `Message` in the current scope
  --> exercises/08_enums/enums1.rs:11:31
   |
2  | enum Message {
   | ------------ variant or associated item `Quit` not found for this enum
...
11 |     println!("{:?}", Message::Quit);
   |                               ^^^^ variant or associated item not found in `Message`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `exercises` (bin "enums1") due to 5 previous errors

这一节的报错已经很明显,每一个错误都有提示,那么根据提示可以看到,Message枚举缺少字段,所以补全字段即可:

error[E0599]: no variant or associated item named `Resize` found for enum `Message` in the current scope
 --> exercises/08_enums/enums1.rs:7:31
  |
2 | enum Message {
  | ------------ variant or associated item `Resize` not found for this enum

根据main 中的逻辑可以得知,需要有Resize,Move,Echo,ChangeColorQuit

补全后:

#[derive(Debug)]
enum Message {
    // TODO: Define a few types of messages as used below.
    Resize,
    Move,
    Echo,
    ChangeColor,
    Quit,
}

将会输出:

Output
Resize
Move
Echo
ChangeColor
Quit


Exercise done ✓
Solution for comparison: solutions/08_enums/enums1.rs
When done experimenting, enter `n` to move on to the next exercise 🦀

Progress: [################################################################>-----------------------------------------------------------------------------------------------------------------------]  33/94
Current exercise: exercises/08_enums/enums1.rs

Go 流水线模式

我将把这篇文章分为两部分,在第一部分我将尝试解释管道的基本构建块,在第二篇文章中我将尝试围绕这个设计构建一个通用库。

我最近在阅读Concurrency In Go时遇到了一种叫做管道处理模式的东西。想法是您可以将逻辑功能分解为多个阶段。每个阶段都进行自己的处理,并将输出传递到下一个阶段进行处理。您可以修改彼此独立的阶段,限制阶段的速率等等。

管道基础知识

来看下这两个函数:

func Multiply(value ,multiplier int) int { 
	return value*multiplier
}
func Add(value,additive int) int { 
	return value+additive
}

这两个函数很简单,只是对数字进行运算并返回它。您可以将这些视为管道的“阶段”。为了完成管道,我们可以将两个阶段结合起来。

ints := []int{1,2,3,4}

for _,v := range ints {
	fmt.Println(multiply(add(multiply(v,2),1),2))
}

在这里可以看到 stage 处理输入,处理后返回相同的类型以供进一步的 stage 处理。

并发管道

这个简单的模型现在可以扩展到利用 go 的通道和 goroutines 来同时执行阶段的处理。在我们这样做之前,我们的管道中必须有以下实体。

  • Generator,它将负责生成管道处理的输入。这是管道的第一阶段。

  • 阶段,可以在其中执行实际处理。

  • Canceller,一种通过管道发出取消或处理结束信号的机制。

让我们从生成器开始

func generator(done <-chan interface{}, integers ...int) <-chan int {
	intStream := make(chan int)
	go func() {
		defer close(intStream)
		for _, i := range integers {
			select {
			case <-done: return
			case intStream <- i:
			}
		}

	}()
	return intStream
}

这个函数将生成一个 goroutine,它将在一个通道上生成数值,并且该函数只是简单地返回那个通道。在此通道上生成的值用作进一步阶段的输入。我们还在函数中传递了 done 通道来退出生成流程,这也称为毒丸模式(Poison Pill Pattern)

扩展相同的想法,我们可以重写乘法和加法函数来并发处理。

func multiply(done <-chan interface{}, intStream <-chan int, multiplier int) chan int {
	multipliedStream := make(chan int)
	go func() {
		defer close(multipliedStream)
		for i := range intStream {
			select {
			case <-done: return
			case multipliedStream <- i * multiplier:
			}
		}
	}()
	return multipliedStream
}

func add(done <-chan interface{}, intStream <-chan int, adder int) chan int {
	addedStream := make(chan int)
	go func() {
		defer close(addedStream)
		for i := range intStream {
			select {
			case <-done:return
			case addedStream <- i * adder:
			}
		}
	}()
	return addedStream
}

旁注:也许我们可以重写这些函数来接受接口或函数,而不是编写两次共享大部分代码的函数,但我试图在这里保持简单。

为了构建管道,我们可以将管道的这些阶段组合为如下形式:

done := make(chan interface{})
intStream := generator(done, 1, 2, 3, 4)

pipeline := add(done, multiply(done, intStream, 2), 5)

for i := range pipeline {
    fmt.Println(i)
}

这段代码将在不同的管道中同时运行阶段,通过 go 通道将它们的结果传递到下一个阶段。我们可以通过阅读最后阶段的频道来获得最终结果。

更实际的用例

好的,但是更实用的东西怎么样?我们可以使用这种设计实现一个shopify的爬虫。目的是抓取shopify页面并抓取有用的信息。

这是我们可以遵循的基本设计算法。

转到应用程序目录页面,例如https://apps.shopify.com/browse/all?page=1

从每个应用程序目录页面抓取不同应用程序的 URL。

访问每个应用 URL 抓取应用信息并填充我们的结构。

遍历分页以转到下一个应用程序目录页面并重复。

在管道方面,我们需要构建这样的东西

让我们定义 App 结构

type App struct {
    Rating     float64
    Review     int
    Name       string
    Category   []string
    AppLink    string
    PageLink   string
}

我没有编写实际的抓取代码来保持简短和简单,但是可以轻松增加抓取逻辑而无需更改代码结构。


type ShopifyScraper struct {}



// Generation Stage, This stage will generate app directory URLs.
func (s *ShopifyScraper) Generate(done <-chan bool) chan App {

	links := make(chan App)
	go func() {
		defer close(links)
		for i := 0; i < 5; i++ {
			select {
			case <-done: return
			case links <- App{PageLink: fmt.Sprintf("https://apps.shopify.com/browse/all?page=%d", i)}:
			}
		}
	}()
	return links
}

// App Directory Scraper Stage, This stage will visit directory page and scrape different app URLs from the page for further processing.
func (s *ShopifyScraper) AppDirectoryScraper(done <-chan bool, input <-chan App) chan App {

	apps := make(chan App, 5)
	go func() {
		defer close(apps)
		for app := range input {

			// Extract Different App URLs by scraping  using app.PageLink

			select {
			case <-done: return
			case apps <- app:
			}
		}
	}()
	return apps
}

// App Scraper Stage, This stage will visit app page and scrape app information
func (s *ShopifyScraper) AppScraper(done <-chan bool, input <-chan App) chan App {
	apps := make(chan App)
	go func() {
		defer close(apps)
		for app := range input {
			
			// Extract Different App Information by scraping by using app.AppLink
			
			select {
			case <-done: return
			case apps <- app:

			}
		}
	}()
	return apps
}

最后,将各个阶段连接在一起以形成管道。

func (s *ShopifyScraper) Do() []AppInfo {

	var result []AppInfo
	done := make(chan bool)
	defer close(done)

	for i := range s.AppScraper(done, s.AppDirectoryScraper(done, s.Generate(done))) {
		result = append(result, i)
	}
	return result
}

在下一篇文章中,我们将尝试将管道构建为一个库,它可以处理 (1) 管道排序 (2) 在多个 goroutine 中同时执行同一阶段 (3) 取消 (4) 可能的错误处理/重试

原文链接

Pipeline Pattern in Go Part 1

Go json 时间格式解析

默认RFC 3339

在Go中,解析json的时间格式默认是RFC 3339,一般情况下 struct 转 json,或 json 转回 struct都没有问题。

type Data struct {
	Name    string    `json:"name"`
	Started time.Time `json:"started"`
}

var jsonStr = []byte(`
{
  "name": "new year",
  "started": "2021-01-01T00:00:00.000+08:00"
}`)

func main() {
	var data Data
	if err := json.Unmarshal(jsonStr, &data); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", data)
}

Z,T

上述代码中,日期和时间使用T分隔开,其实 RFC 3339规范中允许使用空格分隔,目前Go的time包还是使用T分隔。

+8:00则表示时区,如果不想使用时区,可以使用Z代替:2021-01-01T00:00:00.000Z

还有一点需要注意的是,在Go中,RFC 3339的定义是:

RFC3339     = "2006-01-02T15:04:05Z07:00"

但是如果这样写,却不行。

t, err := time.Parse(time.RFC3339, time.RFC3339)
if err != nil {
	log.Fatal(err)
}
fmt.Println(t)

会报错:”parsing time “2006-01-02T15:04:05Z07:00”: extra text: 07:00”。

但是如果是把上述json的时间值修改成2021-01-01T00:00:00.000Z就没有问题。

格式

默认的时间格式是RFC 3339,如果想换成其他格式,需要自己实现。

先看一下时间格式的定义

一般情况下格式化时间可以这样做:

t, err := time.Parse("2006-01-02", "2021-01-01")

但是如果想从json中解析成不同格式,需要自己实现json.Unmarshaler接口。

type MyTime struct {
	time.Time
}

const layout = "2006-01-02"

func (ct *MyTime) UnmarshalJSON(b []byte) (err error) {
	s := strings.Trim(string(b), "\"")
	if s == "null" {
		ct.Time = time.Time{}
		return
	}
	ct.Time, err = time.Parse(layout, s)
	return
}

然后,就可以使用自己的时间类型了。


type Data struct {
	Name    string `json:"name"`
	Started MyTime `json:"started"`
}

var jsonStr = []byte(`
{
  "name": "new year",
  "started": "2021-01-01"
}`)

func main() {
	var data Data
	if err := json.Unmarshal(jsonStr, &data); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", data)
}

优雅的关闭Go TCP Server

本文将讨论如何在Go中优雅地关闭TCP服务器。虽然服务器通常不会停止运行(直到进程终止),但在某些情况下(例如在测试中),以有序的方式关闭它们是有用的。

Go TCP server的高级结构

我们先快速回顾一下Go中实现的TCP服务器的高层结构。Go在套接字的顶层提供了一些方便的抽象。下面是典型服务器的伪代码:

listener := net.Listen("tcp", ... address ...)
for {
  conn := listener.Accept()
  go handler(conn)
}

其中handler是一个阻塞函数,它等待来自客户端的命令,执行所需的处理,并将响应发送回。

鉴于这种结构,我们应该明白“关闭服务器”的含义。服务器在任何给定时间都执行两种不同的功能:

  1. 它监听新的连接
  2. 它处理已有连接

很明显,我们可以停止监听新的连接,从而处理(1);但是现有的连接呢?

不幸的是,这里没有简单的答案。TCP协议级别太低,无法最终解决此问题。如果我们想设计一个广泛适用的解决方案,我们必须保守。具体来说,最安全的方法是关闭服务器以等待客户端关闭其连接。这是我们将首先研究的方法。

步骤1:等待客户端连接关闭

在这个解决方案中,我们将显式地关闭侦听器(停止接受新连接),等待客户端结束其连接。这是一种保守的方法,但它在许多实际需要关闭服务器的场景(如测试)中非常有效。在测试中,使所有客户端在预期服务器关闭之前关闭连接很容易。

我将一段一段地展示代码,但是这里提供了完整的可运行代码示例。让我们从服务器类型和构造函数开始:

type Server struct {
  listener net.Listener
  quit     chan interface{}
  wg       sync.WaitGroup
}

func NewServer(addr string) *Server {
  s := &Server{
    quit: make(chan interface{}),
  }
  l, err := net.Listen("tcp", addr)
  if err != nil {
    log.Fatal(err)
  }
  s.listener = l
  s.wg.Add(1)
  go s.serve()
  return s
}

NewServer创建一个新的Server实例,该服务器在后台goroutine中侦听新连接。除了net.Listener之外,Server结构还包含一个用于发出关闭信号的通道和一个等待组,等待服务器的所有goroutine实际完成。

以下是构造函数调用的服务方法:

func (s *Server) serve() {
  defer s.wg.Done()

  for {
    conn, err := s.listener.Accept()
    if err != nil {
      select {
      case <-s.quit:
        return
      default:
        log.Println("accept error", err)
      }
    } else {
      s.wg.Add(1)
      go func() {
        s.handleConection(conn)
        s.wg.Done()
      }()
    }
  }
}

这是一个标准的Accept循环,除了select。此select所做的是在接受错误输出时检查(以非阻塞方式)s.quit通道上是否存在事件(例如发送或关闭)。如果有,则意味着错误是由我们关闭侦听器引起的,并且服务将安静地返回。如果Accept返回时没有错误,则运行连接处理程序[1]。

下面是告诉服务器正常关闭的Stop方法:

func (s *Server) Stop() {
  close(s.quit)
  s.listener.Close()
  s.wg.Wait()
}

首先关闭s.quit通道。然后它关闭监听器。这将导致服务中的Accept调用返回错误。由于s.quit此时已关闭,服务将返回,不再处理。

Stop方法的最后一行是在s.wg,这也是关键。注意,serve通知等待组它在返回时完成。但这不是我们等待的唯一一次。对handleConnection的每个调用也由wg add/done包装。因此,Stop将阻塞直到所有处理程序都返回,而serve将停止接受新连接。这是一个安全的关闭点。

为了完整起见,这里是handleConnection;这里的handleConnection只读取客户端数据并将其记录下来,而不发送任何数据。当然,这部分代码对于每个服务器都是不同的:

func (s *Server) handleConection(conn net.Conn) {
  defer conn.Close()
  buf := make([]byte, 2048)
  for {
    n, err := conn.Read(buf)
    if err != nil && err != io.EOF {
      log.Println("read error", err)
      return
    }
    if n == 0 {
      return
    }
    log.Printf("received from %v: %s", conn.RemoteAddr(), string(buf[:n]))
  }
}

使用此服务器很简单:

s := NewServer(addr)
// do whatever here...
s.Stop()

回想一下,NewServer返回一个服务器,但不阻塞。s.Stop确实会阻塞。在测试中,您要做的是:

  1. 确保与服务器交互的所有客户端都已关闭其连接。
  2. 等待s.Stop。

步骤2:主动关闭打开的客户端连接

在步骤1中,我们希望所有客户端在声明关闭进程成功之前关闭其连接。在这里,我们将看到一种更激进的方法,在Stop()中,服务器将主动尝试关闭打开的客户端连接。首先,我将介绍一种既简单又健壮的技术,以牺牲一些性能为代价。之后,我们将讨论一些替代方案。

此步骤的完整代码。与步骤1相同,只是handleConection的代码:

func (s *Server) handleConection(conn net.Conn) {
  defer conn.Close()
  buf := make([]byte, 2048)
ReadLoop:
  for {
    select {
    case <-s.quit:
      return
    default:
      conn.SetDeadline(time.Now().Add(200 * time.Millisecond))
      n, err := conn.Read(buf)
      if err != nil {
        if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
          continue ReadLoop
        } else if err != io.EOF {
          log.Println("read error", err)
          return
        }
      }
      if n == 0 {
        return
      }
      log.Printf("received from %v: %s", conn.RemoteAddr(), string(buf[:n]))
    }
  }
}

此处理程序为每个套接字读取设置一个截止日期。这里的截止时间是200毫秒,但可以设置为对您的特定应用程序有意义的任何其他时间。如果读取返回超时,则表示客户端在超时期间处于空闲状态,连接可以安全关闭。所以循环的每次迭代都会检查s.quit并返回是否存在事件。

这种方法是健壮的,因为我们(很可能)不会在客户端主动发送消息时关闭连接。它也很简单,因为它将所有额外的逻辑限制为handleConnection

当然,这里还有一些性能损耗。首先,每200毫秒发出一次conn.Read调用,这比单个阻塞调用稍慢;不过,我认为这可以忽略不计。更严重的是,每一个Stop请求都会延迟200毫秒。在大多数情况下,如果我们想关闭服务器,这可能是可以的,但是可以根据特定的协议需要调整截止时间。

这种设计的另一种方法是跟踪handleconaction外部所有打开的连接,并在调用Stop时强制关闭它们。这将可能是更高效的,以实现复杂性和一些缺乏鲁棒性为代价。这样的Stop很容易在客户端主动发送数据时关闭连接,从而导致客户端错误。

为了获得正确路径上的灵感,我们可以查看标准库的http.Server.Shutdown方法,其文档如下:

Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down

“idle”在这里是什么意思?大致上,客户端已经有一段时间没有发送任何请求了。HTTP服务器比一般的TCP服务器有优势,因为它是一个更高级别的协议,所以它知道客户端通信模式。在不同的协议中,不同的关闭策略可能是有意义的。

另一个例子是服务端发起消息的协议,或者至少其中一些消息。例如,给定的连接可能处于客户端等待服务器发送某个事件的状态。服务端关闭此连接通常是安全的,无需等待任何东西。

结论

我将用两个一般准则来总结这篇文章:

  1. 尽可能安全地关闭
  2. 考虑更高层次的协议

我通常在编写测试时遇到关闭TCP服务器的需要。我希望每个测试都是独立的,并在测试完成后进行清理,包括所有的客户端-服务器连接和监听服务器。对于这个场景,步骤1非常有效。关闭所有客户端连接后,Server.Stop将立即返回。

[^1]: 注意使用WaitGroup的模式:wg.Add(1)是在go语句启动goroutine之前调用的。这是在启动go s.serve()之前在构造函数中完成的。这种方式对安全很重要。如果我们在goroutine内部调用wg.Add(1),在goroutine有机会运行之前调用wg.Wait()的执行序列可能会发生;因为在这种情况下,Wait组中尚未添加任何内容,Wait将返回;这显然不是我们想要的。

【译】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 分支 - 变基