polaris

专注互联网技术

182017
 

介绍

Go 1.7,testing 包在 T 和 B 类型上引入了一个 Run 方法,允许创建子测试和子基准测试。子测试和子基准测试的引入可以更好地处理故障(failures),细化控制从命令行运行的测试,并行控制,并且经常会使代码更简单、更易于维护。

Table-driven 测试

在详细介绍之前,首先讨论在 Go 中编写测试的常用方法。 一系列相关验证可以通过循环遍历一系列测试用例来实现:

func TestTime(t *testing.T) {
    testCases := []struct {
        gmt  string
        loc  string
        want string
    }{
        {"12:31", "Europe/Zuri", "13:31"},     // incorrect location name
        {"12:31", "America/New_York", "7:31"}, // should be 07:31
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {
        loc, err := time.LoadLocation(tc.loc)
        if err != nil {
            t.Fatalf("could not load location %q", tc.loc)
        }
        gmt, _ := time.Parse("15:04", tc.gmt)
        if got := gmt.In(loc).Format("15:04"); got != tc.want {
            t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
        }
    }
}

通常称为 table-driven(表格驱动) 测试,相比每次测试重复相同代码,减少了重复代码的数量,并且可以直接添加更多的测试用例。

Table-driven 基准测试

在 Go 1.7 之前,不可能使用相同的 table-driven 方法进行基准测试。 基准测试对整个函数的性能进行测试,因此迭代基准测试只是将它们整体作为一个基准测试。

常见的解决方法是定义单独的顶级基准,每个基准用不同的参数调用共同的函数。 例如,在 1.7 之前,strconv 包的 AppendFloat 的基准测试看起来像这样:

func benchmarkAppendFloat(b *testing.B, f float64, fmt byte, prec, bitSize int) {
    dst := make([]byte, 30)
    b.ResetTimer() // Overkill here, but for illustrative purposes.
    for i := 0; i < b.N; i++ {
        AppendFloat(dst[:0], f, fmt, prec, bitSize)
    }
}

func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909, 'g', -1, 64) }
func BenchmarkAppendFloat(b *testing.B)        { benchmarkAppendFloat(b, 339.7784, 'g', -1, 64) }
func BenchmarkAppendFloatExp(b *testing.B)     { benchmarkAppendFloat(b, -5.09e75, 'g', -1, 64) }
func BenchmarkAppendFloatNegExp(b *testing.B)  { benchmarkAppendFloat(b, -5.11e-95, 'g', -1, 64) }
func BenchmarkAppendFloatBig(b *testing.B)     { benchmarkAppendFloat(b, 123456789123456789123456789, 'g', -1, 64) }
...

使用 Go 1.7 中提供的 Run 方法,现在将同一组基准表示为单个顶级基准:

func BenchmarkAppendFloat(b *testing.B) {
    benchmarks := []struct{
        name    string
        float   float64
        fmt     byte
        prec    int
        bitSize int
    }{
        {"Decimal", 33909, 'g', -1, 64},
        {"Float", 339.7784, 'g', -1, 64},
        {"Exp", -5.09e75, 'g', -1, 64},
        {"NegExp", -5.11e-95, 'g', -1, 64},
        {"Big", 123456789123456789123456789, 'g', -1, 64},
        ...
    }
    dst := make([]byte, 30)
    for _, bm := range benchmarks {
        b.Run(bm.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                AppendFloat(dst[:0], bm.float, bm.fmt, bm.prec, bm.bitSize)
            }
        })
    }
}

每次调用 Run 方法创建一个单独的基准测试。调用 Run 方法的基准函数只运行一次,不进行性能度量。

新代码行数更多,但是更可维护,更易读,并且与通常用于测试的 table-driven 方法一致。 此外,共同的 setup 代码现在在 Run 之间共享,而不需要重置定时器。

Table-driven 用于子测试

Go 1.7 还引入了一种用于创建子测试的 Run 方法。这个测试是我们上面使用子测试的例子的重写版本:

func TestTime(t *testing.T) {
    testCases := []struct {
        gmt  string
        loc  string
        want string
    }{
        {"12:31", "Europe/Zuri", "13:31"},
        {"12:31", "America/New_York", "7:31"},
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {
        t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) {
            loc, err := time.LoadLocation(tc.loc)
            if err != nil {
                t.Fatal("could not load location")
            }
            gmt, _ := time.Parse("15:04", tc.gmt)
            if got := gmt.In(loc).Format("15:04"); got != tc.want {
                t.Errorf("got %s; want %s", got, tc.want)
            }
        })
    }
}

首先要注意的是两个实现的输出差异。原始实现打印:

--- FAIL: TestTime (0.00s)
    time_test.go:62: could not load location "Europe/Zuri”

即使有两个错误,测试停止在对 Fatal 的调用上,而第二个测试不会运行。

而使用 Run 的版本两个都执行了:

--- FAIL: TestTime (0.00s)
    --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
        time_test.go:84: could not load location
    --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
        time_test.go:88: got 07:31; want 7:31

Fatal 及其相关方法导致子测试被跳过,但不会跳过其父测试其他的子测试。

另一个需要注意的点是新实现版本中的错误消息较短。由于子测试名称可以唯一标识,因此无需在错误消息中再次进行标识。

使用子测试或子基准还有其他好处,如以下部分所述。

运行特定的测试或基准测试

可以使用 -run 或 -bench 标志在命令行中标识子测试或子基准测试。两个标志都采用一个斜杠分隔的正则表达式列表,它们与子测试或子基准测试的全名的相应部分相匹配。

子测试或子基准测试的全名是一个斜杠分隔的名称列表,以顶级名称开始。该名称开始是顶级测试或基准测试的相应函数名称,其他部分是 Run 的第一个参数。为了避免显示和解析问题,名称会通过下划线替换空格并转义不可打印的字符来清理。对传递给 -run 或 -bench 标志的正则表达式应用相同的清理规则。

看一些例子:

使用欧洲时区运行测试:

$ go test -run=TestTime/"in Europe"
--- FAIL: TestTime (0.00s)
    --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
        time_test.go:85: could not load location

仅仅运行时间在午后的测试:(Go 语言中文网注:我本地测试,必须转义 go test -run=Time/12:\[0-9\] -v)

$ go test -run=Time/12:[0-9] -v
=== RUN   TestTime
=== RUN   TestTime/12:31_in_Europe/Zuri
=== RUN   TestTime/12:31_in_America/New_York
--- FAIL: TestTime (0.00s)
    --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
        time_test.go:85: could not load location
    --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
        time_test.go:89: got 07:31; want 7:31

也许有点令人惊讶,使用 -run = TestTime/New_York 将不会匹配任何测试。这是因为位置名称中存在的斜线也被视为分隔符。需要这么使用:

$ go test -run=Time//New_York
--- FAIL: TestTime (0.00s)
    --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
        time_test.go:88: got 07:31; want 7:31

注意 // 在传递给 -run 的字符串中,在时区名称 America/New_York 中的 / 被处理了,就好像它是一个子测试分隔符。模式(TestTime)的第一个正则表达式匹配顶级测试。 第二个正则表达式(空字符串)匹配任何内容,在这 case 中,是时间和位置的洲部分。 第三个正则表达式(New_York)匹配位置的城市部分。

将名称中的斜杠视为分隔符可以让用户重构测试的层次结构,而无需更改命名。它也简化了转义规则。用户应避免在名称中使用斜扛,如果出现问题,请使用反斜杠替换它们。

唯一的序列号附加到不唯一的测试名称。因此,如果没有更好的子测试命名方案,则可以将空字符串传递给 Run,并且可以通过序列号轻松识别子测试。

Setup 和 Tear-down

子测试和子基准测试可用于管理常见的 setup 和 tear-down 代码:

func TestFoo(t *testing.T) {
    // <setup code>
    t.Run("A=1", func(t *testing.T) { ... })
    t.Run("A=2", func(t *testing.T) { ... })
    t.Run("B=1", func(t *testing.T) {
        if !test(foo{B:1}) {
            t.Fail()
        }
    })
    // <tear-down code>
}

当运行测试时,Setup 和 Tear-down 代码运行且最多运行一次。即使任何一个子测试调用了 Skip,Fail 或 Fatal,也适用。

并行度控制

子测试允许对并行性进行细粒度控制。为了理解如何使用子测试进行并行控制,得先理解并行测试的语义。

每个测试都与一个测试函数相关联。如果测试函数调用了其 testing.T 实例上的 Parallel 方法,则测试称为并行测试。并行测试从不与顺序测试同时运行,直到顺序测试返回,并行测试才继续运行。-parallel 标志定义可并行运行的最大并行测试数。

一个测试被堵塞,直到其所有的子测试都已完成。这意味着在一个测试中(TestXXX 函数中),在并行测试完成后,顺序测试才会执行。

对于由 Run 和 顶级测试 创建的测试,此行为是相同的。实际上,顶级测试是隐式的主测试 (master test) 的子测试。

并行运行一组测试

上述语义允许并行地运行一组测试,但不允许其他并行测试:

func TestGroupedParallel(t *testing.T) {
    for _, tc := range testCases {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            t.Parallel()
            if got := foo(tc.in); got != tc.out {
                t.Errorf("got %v; want %v", got, tc.out)
            }
            ...
        })
    }
}

在由 Run 启动的所有并行测试完成之前,外部测试将不会完成。因此,没有其他并行测试可以并行地运行这些并行测试。

请注意,我们需要复制 range 变量以确保 tc 绑定到正确的实例。(因为 range 会重用 tc)

并行测试后的清理

在前面的例子中,根据语义,等待一组并行测试完成之后,其他测试才会开始。在一组共享共同资源的并行测试之后,可以使用相同的技术进行清理:

func TestTeardownParallel(t *testing.T) {
    // <setup code>
    // This Run will not return until its parallel subtests complete.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", parallelTest1)
        t.Run("Test2", parallelTest2)
        t.Run("Test3", parallelTest3)
    })
    // <tear-down code>
}

等待一组并行测试的行为与上一个示例的行为相同。

结论

Go 1.7 加入子测试和子基准测试可以让您以自然的方式编写结构化测试和基准测试,将其很好地融入到现有的工具中。早期版本的 testing 包具有1级层次结构:包级测试由一组单独的测试和基准测试组成。现在,这种结构已经被递归扩展到这些单独的测试和基准测试中。实际上,在实施过程中,顶级测试和基准测试被追踪,就像它们是隐式的主 测试和基准测试 的子测试和子基准测试:在所有级别的处理都是一样的。

为测试定义此结构的能力使得可以对特定测试用例进行细粒度的执行,共享 setup 和 teardown,并更好地控制测试并行性。 如果你发现了什么其他用途,请分享。

本文由 徐新华 翻译。来自 Go语言中文网博客

原文:Using Subtests and Sub-benchmarks

092017
 

排序算法是一种采用列表或数组并以特定顺序对其元素进行重新排序的算法。有几十种不同的排序算法,如果你已经学习了计算机科学,那么你至少熟悉了其中的一些算法。 它们也是很受欢迎的面试问题,所以在重要面试前不要因为它而伤心。

这是一个大多数常见的排序算法的小型引擎,实例采用 Golang 实现。

冒泡排序

冒泡排序是最基本的就地排序算法,几乎每个人都很熟悉。 它具有 O(n²) 最坏情况和平均时间复杂度,这使得它在大型列表中效率低下。它的实现非常简单。

在循环中,从第一个元素到第 n 个(n = len(items))迭代数组。比较相邻的值,如果它们的顺序错误,交换它们。 您可以通过在每次迭代后将 n 递减 1 来优化算法。

时间复杂度:

* 最坏情况下:O(n²)
* 平均情况下:O(n²)
* 最好情况下:O(n)

空间复杂度:

* 最坏情况下:O(1)

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    bubbleSort(items)
    fmt.Println(items)
}

func bubbleSort(items []int) {
    var (
        n       = len(items)
        swapped = true
    )
    for swapped {
        swapped = false
        for i := 0; i < n-1; i++ {
            if items[i] > items[i+1] {
                items[i+1], items[i] = items[i], items[i+1]
                swapped = true
            }
        }
        n = n - 1
    }
}

选择排序

选择排序是另一种简单的平均情况 O(n²) 就地分拣算法。该算法将列表分为两个子列表,一个用于已排序的列表,该列表开始为空,并从列表的开头从左到右构建,第二个子列表用于剩余的未排序的元素。

选择排序可以通过两个嵌套 for 循环来实现。外部循环遍历列表 n 次(n = len(items))。内部循环将始终以外部循环的当前迭代器值开始(因此在每个迭代中,它将从列表中的更右侧的位置开始),并找出子列表的最小值。使用找到的最小值交换子列表的第一项。

时间复杂度:

* 最坏情况下:O(n²)
* 平均情况下:O(n²)
* 最好情况下:O(n²)

空间复杂度:

* 最坏情况下:O(1)

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    selectionSort(items)
    fmt.Println(items)
}

func selectionSort(items []int) {
    var n = len(items)
    for i := 0; i < n; i++ {
        var minIdx = i
        for j := i; j < n; j++ {
            if items[j] < items[minIdx] {
                minIdx = j
            }
        }
        items[i], items[minIdx] = items[minIdx], items[i]
    }
}

插入排序

插入排序是简单的就地 O(n²) 排序算法。同样,它在大型列表中效率较低,但它具有很少有的优点:

* 自适应:时间复杂度随着已经基本排序的列表而减少 – 如果每个元素不超过其最终排序位置的 k 个位置,则 O(nk)
* 稳定:相等值的索引的相对位置不变
* 就地:只需要一个常数 O(1) 的额外的内存空间
* 在实践中比泡沫或选择排序更有效

时间复杂度:

* 最坏情况下:O(n²)
* 平均情况下:O(n²)
* 最好情况下:O(n)

空间复杂度:

* 最坏情况下:O(1)

实现是非常自然的,因为它的工作方式类似于你如何排序手中的排,如果你正在玩一个纸牌游戏。

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    insertionSort(items)
    fmt.Println(items)
}

func insertionSort(items []int) {
    var n = len(items)
    for i := 1; i < n; i++ {
        j := i
        for j > 0 {
            if items[j-1] > items[j] {
                items[j-1], items[j] = items[j], items[j-1]
            }
            j = j - 1
        }
    }
}

希尔排序(shell sort)

希尔排序是插入排序的泛化。这是一个有趣的排序算法,通过将列表排列成一组交错排序的子列表来工作。

首先,选择一连串的间隙。有许多不同的公式来产生间隙序列,算法的平均时间复杂度取决于这个变量。例如,我们选择 (2 ^ k ) – 1,前缀为1,这将给我们[1,3,7,15,31,63 ...]。 反转顺序:[...,63,31,15,7,3,q]。

现在遍历颠倒的间隙列表,并在每个子列表中使用插入排序。所以在第一次迭代中,每第63个元素都应用插入排序。在第二次迭代中,每31个元素应用插入排序。所以一路下来到1。最后一次迭代将在整个列表中运行插入。

时间复杂度:

* 最坏情况下:O(n(log(n))²)
* 平均情况下:依赖于间隙序列
* 最好情况下:O(n(log(n)))

空间复杂度:

* 最坏情况下:O(1)

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    shellshort(items)
    fmt.Println(items)
}

func shellshort(items []int) {
    var (
        n    = len(items)
        gaps = []int{1}
        k    = 1
    )

    for {
        gap := pow(2, k) + 1
        if gap > n-1 {
            break
        }
        gaps = append([]int{gap}, gaps...)
        k++
    }
    for _, gap := range gaps {
        for i := gap; i < n; i += gap {
            j := i
            for j > 0 {
                if items[j-gap] > items[j] {
                    items[j-gap], items[j] = items[j], items[j-gap]
                }
                j = j - gap
            }
        }
    }
}

func pow(a, b int) int {
    p := 1
    for b > 0 {
        if b&1 != 0 {
            p *= a
        }
        b >>= 1
        a *= a
    }
    return p
}

梳排序

梳排序是冒泡排序算法的改进。虽然冒泡排序总是比较相邻元素(gap = 1),梳排序以 gap = n/1.3 开始,其中 n = len(items),并在每次迭代时缩小 1.3 倍。

这种改进背后的想法是消除所谓的海龟(靠近列表末尾的小值)。最后的迭代与 gap = 1 的简单冒泡排序相同。

时间复杂度:

* 最坏情况下:O(n²)
* 平均情况下:O(n²/2^p) (p 是增量的数量)
* 最好情况下:O(n(log(n)))

空间复杂度:

* 最坏情况下:O(1)

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    combsort(items)
    fmt.Println(items)
}

func combsort(items []int) {
    var (
        n       = len(items)
        gap     = len(items)
        shrink  = 1.3
        swapped = true
    )

    for swapped {
        swapped = false
        gap = int(float64(gap) / shrink)
        if gap < 1 {
            gap = 1
        }
        for i := 0; i+gap < n; i++ {
            if items[i] > items[i+gap] {
                items[i+gap], items[i] = items[i], items[i+gap]
                swapped = true
            }
        }
    }
}

归并排序

归并排序是一种非常有效的通用排序算法。这是分治算法的典型应用,这意味着列表被递归地分解成更小的列表,这些列表被排序然后被递归地组合以形成完整的列表。

来自维基百科:在概念上,合并排序的工作方式如下:
1.将未排序的列表划分为n个子列表,每个子列表包含1个元素(1个元素的列表被视为排序)。
2.重复合并子列表以生成新的排序子列表,直到只剩下1个子列表。 这将是排序列表。

时间复杂度:

* 最坏情况下:O(n(log(n)))
* 平均情况下:O(n(log(n)))
* 最好情况下:O(n(log(n)))

空间复杂度:

* 最坏情况下:O(n)

package main

import (
    "fmt"
)

func main() {
    items := []int{4, 202, 3, 9, 6, 5, 1, 43, 506, 2, 0, 8, 7, 100, 25, 4, 5, 97, 1000, 27}
    sortedItems := mergeSort(items)
    fmt.Println(sortedItems)
}

func mergeSort(items []int) []int {
    var n = len(items)

    if n == 1 {
        return items
    }

    middle := int(n / 2)
    var (
        left  = make([]int, middle)
        right = make([]int, n-middle)
    )
    for i := 0; i < n; i++ {
        if i < middle {
            left[i] = items[i]
        } else {
            right[i-middle] = items[i]
        }
    }

    return merge(mergeSort(left), mergeSort(right))
}

func merge(left, right []int) (result []int) {
    result = make([]int, len(left)+len(right))

    i := 0
    for len(left) > 0 && len(right) > 0 {
        if left[0] < right[0] {
            result[i] = left[0]
            left = left[1:]
        } else {
            result[i] = right[0]
            right = right[1:]
        }
        i++
    }

    // Either left or right may have elements left; consume them.
    // (Only one of the following loops will actually be entered.)
    for j := 0; j < len(left); j++ {
        result[i] = left[j]
        i++
    }
    for j := 0; j < len(right); j++ {
        result[i] = right[j]
        i++
    }

    return
}
062017
 

我是 Prometheus 的大粉丝。 我在家庭和工作中都使用了很多,并且非常喜欢深入了解我的系统在任何时刻的工作情况。 最广泛使用的 Prometheus 商家之一是 node_exporter:可以从类 UNIX 机器中提取各种指标的守护进程。

在我浏览仓库时,我注意到 open issue,要求向 node_exporter 添加 WiFi 指标。 这个想法吸引了我,我意识到我一定会在我的 Linux 笔记本电脑上使用这样一个功能。 我开始探索在 Linux 上检索WiFi 设备信息的选项。

经过几周的实验(包括旧版 ioctl() 无线扩展 API),我创作了两个 Go 包,它们可以共同工作,在 Linux 上与 WiFi 设备进行交互:

* netlink:提供对 Linux netlink 套接字的底层访问。
* wifi:提供对 IEEE 802.11 WiFi 设备操作和统计信息的访问。

这一系列的文章将描述在 Go 中实现这些包时学到的一些经验教训,希望能够为选择这两个库进行试验的人提供一个很好的参考。

本系列的伪代码将使用 Go 的 x/sys/unix 包和我的 netlink 和 wifi 包的类型。 我打算把这个系列分解如下(链接来自更多的文章):

* 第 1 部分:netlink(这篇文章):netlink 的介绍。
* 第 2 部分:通用 netlink:通用 netlink 简介,一个 netlink 家族,旨在简化新家庭的创建。
* 第 3 部分:包 netlink,genetlink 和 wifi:使用 Go 驱动与 netlink,通用 netlink 和 nl80211 的交互。

什么是 netlink?

Netlink 是一个 Linux 内核进程间通信机制,可实现用户空间进程与内核之间的通信,或多个用户空间进程通讯。 Netlink 套接字是启用此通信的原语。

这篇文章将提供关于 netlink 套接字,消息,多播组和属性的基本知识。 此外,这篇文章将重点介绍用户空间和内核之间的通信,而不是两个用户空间进程之间的通信。

创建 netlink 套接字

Netlink 使用标准 BSD 套接字 API。 对于在 C 中进行网络编程的人来说,这应该是非常熟悉的。如果您不熟悉 BSD 套接字,我建议您阅读优秀的“Beej 网络编程指南”来了解本主题。

重要的是要注意,netlink 通信永远不会遍历本地主机(local host)。 记住这一点,让我们开始深入了解 netlink 套接字的工作原理!

要与 netlink 进行通信,必须打开 netlink 套接字。 这是使用 socket() 系统调用完成的:

fd, err := unix.Socket(
    // Always used when opening netlink sockets.
    // 打开 netlink 套接字时始终使用。
    unix.AF_NETLINK,
    // Seemingly used interchangeably with SOCK_DGRAM,
    // but it appears not to matter which is used.
    // 似乎与 SOCK_DGRAM 可互换使用,使用哪个并不重要。
    unix.SOCK_RAW,
    // The netlink family that the socket will communicate
    // with, such as NETLINK_ROUTE or NETLINK_GENERIC.
    // 套接字与之通信的 netlink 系列,如 NETLINK_ROUTE 或 NETLINK_GENERIC。
    family,
)

family 参数指定一个特定的 netlink 系列:本质上是可以使用 netlink 套接字进行通信的内核子系统。 这些系列可能会提供诸如此类的功能。

* NETLINK_ROUTE:操纵 Linux 的网络接口,路由,IP地址等
* NETLINK_GENERIC:简化添加新的 netlink 系列的构建块,如 nl80211,Open vSwitch 等。

一旦创建套接字,必须调用 bind() 来准备发送和接收消息。

err := unix.Bind(fd, &unix.SockaddrNetlink{
    // Always used when binding netlink sockets.
    // 绑定 netlink 套接字时始终使用。
    Family: unix.AF_NETLINK,
    // A bitmask of multicast groups to join on bind.
    // Typically set to zero.
    // 绑定的多播组加入的位掩码。通常设置为零。
    Groups: 0,
    // If you'd like, you can assign a PID for this socket
    // here, but in my experience, it's easier to leave
    // this set to zero and let netlink assign and manage
    // PIDs on its own.
    // 如果你愿意,你可以在这里给这个套接字分配一个PID,但是根据我的经验,将这个设置保留为零是很容易的,让 netlink 自己分配和管理 PID。
    Pid: 0,
})

此时,netlink 套接字现在可以发送和接收来自内核的消息。

Netlink 消息格式

Netlink 消息遵循非常特殊的格式。 所有消息必须与 4 字节边界对齐。 例如,16 字节的消息必须按原样发送,但是 17 字节的消息必须被填充到 20 个字节。

非常重要的是要注意,与典型的网络通信不同,netlink 使用主机字节顺序来编码和解码整数,而不是普通的网络字节顺序(大端)。 因此,必须在数据的字节和整数表示之间转换的代码必须牢记这一点。

Netlink 消息头使用以下格式:(来自 RFC 3549 的图):

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Process ID (PID) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这些字段包含以下信息:

* 长度(32位):整个消息的长度,包括报头和有效载荷(消息体)。
* 类型(16位):消息包含什么样的信息,如错误,multi-part 消息的结束等。
* 标志(16位):指示消息是请求的位标志,如 multi-part ,请求确认等。
* 序列号(32位):用于关联请求和响应的数字;每个请求递增。
* 进程ID(PID)(32位):有时称为端口号;用于唯一标识特定 netlink 套接字的数字; 可能是也可能不是进程的ID。

最后,有效载荷可能会立即跟随 netlink 消息头。 再次注意,有效载荷必须填充到 4 字节的边界。

向内核发送请求的示例 netlink 消息可能类似于 Go 中的以下内容:

msg := netlink.Message{
    Header: netlink.Header{
        // Length of header, plus payload.
        Length: 16 + 4,
        // Set to zero on requests.
        Type: 0,
        // Indicate that message is a request to the kernel.
        Flags: netlink.HeaderFlagsRequest,
        // Sequence number selected at random.
        Sequence: 1,
        // PID set to process's ID.
        PID: uint32(os.Getpid()),
    },
    // An arbitrary byte payload. May be in a variety of formats.
    Data: []byte{0x01, 0x02, 0x03, 0x04},
}

发送和接收 netlink 消息

现在我们熟悉了netlink 套接字的一些基础知识,我们可以使用套接字发送和接收数据。

一旦一个消息已经准备好,它可以使用 sendto() 发送到内核:

// Assume messageBytes produces a netlink request message (like the
// one shown above) with the specified payload.
b := messageBytes([]byte{0x01, 0x02, 0x03, 0x04})
err := unix.Sendto(b, 0, &unix.SockaddrNetlink{
    // Always used when sending on netlink sockets.
    Family: unix.AF_NETLINK,
})

对 netlink 的只读请求通常不需要任何特殊权限。 使用 netlink 修改子系统状态或需要锁定其内部状态的操作通常需要提升权限。 这可能意味着以 root 身份运行该程序或使用 CAP_NET_ADMIN 来执行以下操作:

* 发送写请求,使用 netlink 更改子系统。
* 使用 NLM_F_ATOMIC 标志发送读取请求,来从 netlink 接收数据的原子快照。

使用 recvfrom() 从 netlink socket 接收消息可能会稍微复杂一些,具体取决于各种因素。Netlink 可能如下回复:

* 非常小或非常大的消息。
* Mulit-part 消息,分为多个部分。
* 显示错误号,当消息头类型为“error”时。

此外,每个消息的序列号和 PID 也应该被验证。使用原始系统调用时,由套接字的用户处理这些情况。

大消息

为了处理大消息,我采用了一种分配单页内存的技术:窥探缓冲区(读取数据但不清除),然后将缓冲区的大小加倍,如果太小,无法读取整个消息。 谢谢 Dominik Honnef 你对这个问题的见解。

为了简便起见,我们省略了错误处理。实际中,请检查你的错误!

b := make([]byte, os.Getpagesize())
for {
    // Peek at the buffer to see how many bytes are available.
    n, _, _ := unix.Recvfrom(fd, b, unix.MSG_PEEK)
    // Break when we can read all messages.
    if n < len(b) {
        break
    }
    // Double in size if not enough bytes.
    b = make([]byte, len(b)*2)
}
// Read out all available messages.
n, _, _ := unix.Recvfrom(fd, b, 0)

理论上说,一个 netlink 消息的大小可能高达~4GiB(32位无符号整数的最大值),但实践中消息要小得多。

Multi-part 消息

对于某些类型的消息,netlink 可以使用“Multi-part 消息”来回复。在这种情况下,最后一条之前的每条消息都将设置“multi”标志。 最后的消息将有一个“done”的类型。

返回 Multi-part 消息时,第一个 recvfrom() 将返回所有设置了“multi”标志的消息。 接下来,必须再次调用 recvfrom() 来检索头文件类型为“done”的最终消息。 这是非常重要的,否则 netlink 将会直接 hang 在后续的请求上,等待调用者取出最后的头部类型为“done”的消息。

Netlink 错误号

如果 netlink 由于任何原因不能满足请求,它将在包含消息头类型为“error”的消息的有效载荷中返回明确的错误号。 这些错误号与 Linux 的经典错误号相同,例如“不存在这样的文件或目录”的ENOENT,“被拒绝的权限”的 EPERM。

如果消息的报头类型指示错误,则错误编号将被编码为消息有效载荷的前4个字节中的带符号的32位整数(注意:也使用系统字节顺序)。

const name = "foo0"
_, err := rtnetlink.InterfaceByName(name)
if err != nil && os.IsNotExist(err) {
    // Error is result of a netlink error number, and can be
    // checked in the usual Go fashion.
    log.Printf("no such device: %q", name)
    return
}

序列号和 PID 验证

为了确保内核的 netlink 回复是响应我们的某个请求,我们还必须在每个接收的消息上验证序列号和 PID。在大多数情况下,这些都应该与发送到内核的请求完全一致。后续请求应该在向 netlink 发送另一个消息之前增加序列号。

PID 验证可能会有所不同,具体取决于几个条件。

* 如果在用户空间中代表组播组接收到消息,则它的 PID 为0,表示消息始发于内核。
* 如果请求被发送到 PID 为 0 的内核,netlink 将在第一个响应中为给定的套接字分配一个PID。该PID应在随后的通信中使用(并被验证)。

假设您没有在 bind() 中指定 PID,则在单个应用程序中打开多个 netlink 套接字时,将该进程的 ID 作为第一个 netlink 套接字的 PID。后续的将由 netlink 从随机数选择。根据我的经验,只要让 netlink 分配所有自身的 PID,并确保跟踪每个套接字分配的数字,这一点要容易得多。

多播组

除了传统的请求/响应套接字范例之外,netlink 套接字还提供多播组,以便在发生某些事件时启用订阅。

可以使用两种不同的方法来连接多播组:

* 在 bind() 期间指定组位掩码。这被认为是 “legacy” 的方法。
* 使用 setsockopt() 连接和断开组。 这是首选,现代的方法。

使用 setsockopt() 连接和断开组是交换单个常量的问题。在Go中,这是使用 uint32 “group” 值完成的。

// Can also specify unix.NETLINK_DROP_MEMBERSHIP to leave
// a group.
const joinLeave = unix.NETLINK_ADD_MEMBERSHIP
// Multicast group ID. Typically assigned using predefined
// constants for various netlink families.
const group = 1
err := syscall.SetSockoptInt(
    fd,
    unix.SOL_NETLINK,
    joinLeave,
    group,
)

一旦加入了一个组,你可以像往常一样使用 recvfrom() 来收听消息。断开组将不会给出给定多播组的更多消息。

Netlink 属性

要在 netlink 套接字上包含我们的引用,我们将讨论 netlink 消息有效载荷的一个非常常见的数据格式:属性。

Netlink 属性是不寻常的,因为它们是LTV(长度,类型,值)格式,而不是典型的TLV(类型,长度,值)。 与 netlink 套接字中的其他整数一样,类型和长度值也以主机字节编码。 最后,netlink 属性也必须填充到 4 字节边界,就像 netlink 消息一样。

每个字段包含以下信息:

* 长度(16位):整个属性的长度,包括长度,类型和值字段。不能设置为4字节边界。 例如,如果长度为17字节,则该属性将被填充到20个字节,但是不应该将3个字节的填充解释为有意义的。
* 类型(16位):属性的类型,通常定义为某些 netlink 系列或标题中的常量。
* 值(可变字节):属性的原始有效负载。可能包含以相同格式存储的嵌套属性。这些嵌套属性可能包含更多的嵌套属性!

netlink 属性中可能有两个特殊的标志,虽然我还没有在我的工作中遇到他们。

* NLA_F_NESTED:指定嵌套属性;用作解析的提示。即使存在嵌套属性,似乎并不总是被使用。
* NLA_F_NET_BYTEORDER:属性数据以网络字节顺序(大端)存储,而不是主机字节顺序。

请参阅给定的 netlink 系列的文档,以确定是否应检查其中一个标志。

概要

现在我们熟悉使用 netlink 套接字和消息,系列中的下一篇文章将基于这些知识来深入通用 netlink。
希望你喜欢这篇文章! 如果您有任何疑问或意见,请随时通过评论,Twitter或Gophers Slack(用户名:mdlayher)进行联系。

更新

2/22/2017:将有关BSD套接字API的背景信息移动到“创建netlink套接字”部分。
2/22/2017:注意到需要root或CAP_NET_ADMIN用于许多netlink写操作,以及使用NLM_F_ATOMIC时。
2/23/2017:注意到在 bind() 中为套接字指定 PID 的能力。
2/27/2017:由于 syscall 被冻结,因此将伪代码更改为使用x/sys/unix而不是syscall。

参考

以下链接经常用作参考,因为我建立了包 netlink,并撰写了这篇文章:
维基百科:Netlink:https://en.wikipedia.org/wiki/Netlink
使用Netlink套接字在Linux内核和用户空间之间进行通信:https://pdfs.semanticscholar.org/6efd/e161a2582ba5846e4b8fea5a53bc305a64f3.pdf
使用Netlink套接字了解和编程:https://people.redhat.com/nhorman/papers/netlink.pdf
Netlink [C] 库(libnl):https://www.infradead.org/~tgr/libnl/doc/core.html

202017
 

静态站点生成器是一种工具,给一些输入(例如,markdown),使用HTML,CSS和JavaScript生成完全静态的网站。

为什么这很酷?一般来说,搭建一个静态网站更容易,而且通常运行也会比较快一些,同时占用资源也更少。虽然静态网站不是所有场景的最佳选择,但是对于大多数非交互型网站(如博客)来说,它们是非常好的。

在这篇文章中,我将讲述我用Go写的静态博客生成器。

动机

您可能熟悉静态站点生成器,比如伟大的Hugo,它具有关于静态站点生成的所有功能。

那么为什么我还要来编写另外一个功能较少的类似工具呢? 原因是双重的。

一个原因是我想深入了解Go,一个基于命令行的静态站点生成器似乎是磨练我技能很好的方式。

第二个原因就是我从来没有这样做过。 我已经完成了平常的Web开发工作,但是我从未创建过一个静态站点生成器。

这听起来很有趣,因为理论上,从我的网站开发背景来看,我满足所有先决条件和技能来构建这样一个工具,,但我从来没有尝试过这样做。

大约2个星期,我实现了它,并且很享受做的过程。 我使用我的博客生成器创建我的博客,迄今为止,它运行良好。

概念

早些时候,我决定采用 markdown 格式写博客,同时保存在 GitHub Repo。这些文章是以文件夹的形式组织的,它们代表博客文章的网址。

对于元数据,如发布日期,标签,标题和副标题,我决定保存在每篇文章的(post.md) meta.yml 文件中,它具有以下格式:

标题:玩BoltDB
简介:“为你的 Go 应用程序寻找一个简单的 key/value 存储器吗?看它足够了!
日期:20.04.2017
标签:
- golang
- go
- boltdb
- bolt

这样,我将内容与元数据分开了,但稍后会发现,其实仍然是将所有内容都放在了同一个地方。

GitHub Repo 是我的数据源。下一步是想功能,我想出了如下功能列表:

* 非常精益(在 gzipped 压缩情况下,入口页1请求应<10K)
* 列表存档
* 在博客文章中使用代码语法高亮和和图像
* tags
* RSS feed(index.xml)
* 可选静态页面(例如 About)
* 高可维护性 – 使用尽可能少的模板
* 针对 SEO 的 sitemap.xml
* 整个博客的本地预览(一个简单的 run.sh 脚本)

相当健康的功能集。 从一开始,对我来说非常重要的是保持一切简单,快速和干净 – 没有任何第三方跟踪器或广告,因为这会影响隐私,并会影响速度。

基于这些想法,我开始制定一个粗略的架构计划并开始编码。

架构概述

应用程序足够简单 高层次的要素有:

* 命令行工具(CLI)
* 数据源(DataSource)
* 生成器(Generators)

在这种场景下,CLI 非常简单,因为我没有在可配置性方面添加任何功能。它基本上只是从DataSource 获取数据,并在其上运行 Generator。

DataSource 接口如下所示:

type DataSource interface {
    Fetch(from, to string) ([]string, error)
}

Generator 接口如下所示:

type Generator interface {
    Generate() error
}

很简单。每个生成器还接收一个配置结构,其中包含生成器所需的所有必要数据。

目前已有 7 个生成器:

* SiteGenerator
* ListingGenerator
* PostGenerator
* RSSGenerator
* SitemapGenerator
* StaticsGenerator
* TagsGenerator

SiteGenerator 是元生成器,它调用所有其他生成器并输出整个静态网站。

目前版本是基于 HTML 模板的,使用的是 Go 的 html/template 包。

实现细节

在本节中,我将只介绍几个有觉得有意思的部分,例如 git DataSource 和不同的 Generators。

数据源

首先,我们需要一些数据来生成我们的博客。如上所述,这些数据存储在 git 仓库。 以下 Fetch 函数涵盖了 DataSource 实现的大部分内容:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    fmt.Printf("Fetching data from %s into %s...\n", from, to)
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    fmt.Print("Fetching complete.\n")
    return dirs, nil
}

使用两个参数调用 Fetch,from 是一个仓库 URL,to 是目标文件夹。 该函数创建并清除目标文件夹,使用 os/exec 加上 git 命令克隆仓库,最后读取文件夹,返回仓库中所有文件的路径列表。

如上所述,仓库仅包含表示不同博客文章的文件夹。 然后将具有这些文件夹路径的数组传递给生成器,它可以为仓库中的每个博客文章执行其相应的操作。

拉开帷幕

Fetch 之后,就是 Generate 阶段。执行博客生成器时,最高层执行以下代码:

ds := datasource.New()
dirs, err := ds.Fetch(RepoURL, TmpFolder)
if err != nil {
    log.Fatal(err)
}
g := generator.New(&generator.SiteConfig{
    Sources:     dirs,
    Destination: DestFolder,
})
err = g.Generate()
if err != nil {
    log.Fatal(err)
}

generator.New 函数创建一个新的 SiteGenerator,这是一个基础生成器,它会调用其他生成器。这里我们提供了仓库中的博客文章目录(数据源)和目标文件夹。

由于每个生成器都实现了上述接口的 Generator,因此 SiteGenerator 有一个 Generate 方法,它返回 error。 SiteGenerator 的 Generate 方法准备目标文件夹,读取模板,准备博客文章的数据结构,注册其他生成器并并发的运行它们。

SiteGenerator 还为博客注册了一些设置信息,如URL,语言,日期格式等。这些设置只是全局常量,这当然不是最漂亮的解决方案,也不是最具可伸缩性的,但很简单,这也是我最高的目标。

文章

博客中最重要的概念是 – 惊喜,惊喜 – 博客文章! 在这个博客生成器的上下文中,它们由以下数据结构表示:

type Post struct {
    Name      string
    HTML      []byte
    Meta      *Meta
    ImagesDir string
    Images    []string
}

这些文章是通过遍历仓库中的文件夹,读取 meta.yml 文件,将 post.md 文件转换为 HTML 并添加图像(如果有的话)创建的。

相当多的工作,但是一旦我们将文章表示为一个数据结构,那么生成文章就会很简单,看起来像这样:

func (g *PostGenerator) Generate() error {
    post := g.Config.Post
    destination := g.Config.Destination
    t := g.Config.Template
    staticPath := fmt.Sprintf("%s%s", destination, post.Name)
    if err := os.Mkdir(staticPath, os.ModePerm); err != nil {
      return fmt.Errorf("error creating directory at %s: %v", staticPath, err)
    }
    if post.ImagesDir != "" {
      if err := copyImagesDir(post.ImagesDir, staticPath); err != nil {
          return err
      }
    }
    if err := writeIndexHTML(staticPath, post.Meta.Title, template.HTML(string(post.HTML)), t); err != nil {
      return err
    }
    return nil
}

首先,我们为该文章创建一个目录,然后我们复制图像,最后使用模板创建该文章的 index.html 文件。

列表创建

当用户访问博客的着陆页时,她会看到最新的文章,其中包含文章的阅读时间和简短描述等信息。 对于此功能和归档,我实现了ListingGenerator,它使用以下配置:

type ListingConfig struct {
    Posts                  []*Post
    Template               *template.Template
    Destination, PageTitle string
}

该生成器的 Generate 方法遍历该文章,组装其元数据,并根据给定的模板创建概要。 然后这些块被附加并写入相应的 index 模板。

我喜欢一个媒体的功能:文章大概阅读时间,所以我实现了我自己的版本,基于一个普通人每分钟读取大约 200 个字的假设。 图像也计入整体阅读时间,并为该帖子中的每个 img 标签添加了一个常量 12 秒。这显然不会对任意内容进行扩展,但对于我惯常的文章应该是一个很好的近似值:

func calculateTimeToRead(input string) string {
    // an average human reads about 200 wpm
    var secondsPerWord = 60.0 / 200.0
    // multiply with the amount of words
    words := secondsPerWord * float64(len(strings.Split(input, " ")))
    // add 12 seconds for each image
    images := 12.0 * strings.Count(input, "<img")
    result := (words + float64(images)) / 60.0
    if result < 1.0 {
        result = 1.0
    }
    return fmt.Sprintf("%.0fm", result)
}

Tags

接下来,要有一种按主题归类和过滤文章的方法,我选择实现一个简单的 tag(标签) 机制。 文章在他们的 meta.yml 文件中有一个标签列表。这些标签应该列在单独的标签页上,并且点击标签后,用户应该看到带有所选标签的文章列表。

首先,我们创建一个从 tag 到 Post 的 map:

func createTagPostsMap(posts []*Post) map[string][]*Post {
result := make(map[string][]*Post)
    for _, post := range posts {
        for _, tag := range post.Meta.Tags {
            key := strings.ToLower(tag)
             if result[key] == nil {
                 result[key] = []*Post{post}
             } else {
                 result[key] = append(result[key], post)
             }
        }
    }
    return result
}

接着有两项任务要实现:

* 标签页
* 所选标签的文章列表

标签(Tag)的数据结构如下所示:

type Tag struct {
    Name  string
    Link  string
    Count int
}

所以,我们有实际的标签(名称),链接到标签的列表页面和使用此标签的文章数量。这些标签是从 tagPostsMap 创建的,然后按 Count 降序排序。

tags := []*Tag{}
for tag, posts := range tagPostsMap {
    tags = append(tags, &Tag{Name: tag, Link: getTagLink(tag), Count: len(posts)})
}
sort.Sort(ByCountDesc(tags))

标签页基本上只是包含在 tags/index.html 文件中的列表。

所选标签的文章列表可以使用上述的 ListingGenerator 来实现。 我们只需要迭代标签,为每个标签创建一个文件夹,选择要显示的帖子并为它们生成一个列表。

Sitemap 和 RSS

为了提高网络的可搜索性,最好建立一个可以由机器人爬取的 sitemap.xml。创建这样的文件是非常简单的,可以使用 Go 标准库来完成。

然而,在这个工具中,我选择使用了 etree 库,它为创建和读取 XML 提供了一个很好的 API。

SitemapGenerator 使用如下配置:

type SitemapConfig struct {
    Posts       []*Post
    TagPostsMap map[string][]*Post
    Destination string
}

博客生成器采用基本的方法来处理 sitemap,只需使用 addURL 函数生成 URL 和图像。

func addURL(element *etree.Element, location string, images []string) {
    url := element.CreateElement("url")
     loc := url.CreateElement("loc")
     loc.SetText(fmt.Sprintf("%s/%s/", blogURL, location))

     if len(images) > 0 {
         for _, image := range images {
            img := url.CreateElement("image:image")
             imgLoc := img.CreateElement("image:loc")
             imgLoc.SetText(fmt.Sprintf("%s/%s/images/%s", blogURL, location, image))
         }
     }
}

在使用 etree 创建XML文档之后,它将被保存到文件并存储在输出文件夹中。

RSS 生成工作方式相同 – 迭代所有文章并为每个文章创建 XML 条目,然后写入 index.xml。

处理静态资源

最后一个概念是静态资源,如 favicon.ico 或静态页面,如 About。 为此,该工具将使用下面配置运行 StaticsGenerator:

type StaticsConfig struct {
    FileToDestination map[string]string
    TemplateToFile    map[string]string
    Template          *template.Template
}

FileToDestination-map 表示静态文件,如图像或 robots.txt,TemplateToFile是从静态文件夹中的模板到其指定的输出路径的映射。

这种配置可能看起来像这样:

fileToDestination := map[string]string{
    "static/favicon.ico": fmt.Sprintf("%s/favicon.ico", destination),
    "static/robots.txt":  fmt.Sprintf("%s/robots.txt", destination),
    "static/about.png":   fmt.Sprintf("%s/about.png", destination),
}
templateToFile := map[string]string{
    "static/about.html": fmt.Sprintf("%s/about/index.html", destination),
}
statg := StaticsGenerator{&StaticsConfig{
FileToDestination: fileToDestination,
   TemplateToFile:    templateToFile,
   Template:          t,
}}

用于生成这些静态资源的代码并不是特别有趣 – 您可以想像,这些文件只是遍历并复制到给定的目标。

并行执行

为了使博客生成器运行更快,所有生成器应该并行执行。正因为此,它们都遵循 Generator 接口, 这样我们可以将它们全部放在一个 slice 中,并发地调用 Generate。

这些生成器都可以彼此独立工作,不使用任何全局可变状态,因此使用 channel 和 sync.WaitGroup 可以很容易的并发执行它们。

func runTasks(posts []*Post, t *template.Template, destination string) error {
    var wg sync.WaitGroup
    finished := make(chan bool, 1)
    errors := make(chan error, 1)
    pool := make(chan struct{}, 50)
    generators := []Generator{}

    for _, post := range posts {
        pg := PostGenerator{&PostConfig{
            Post:        post,
             Destination: destination,
             Template:    t,
        }}
        generators = append(generators, &pg)
    }

    fg := ListingGenerator{&ListingConfig{
        Posts:       posts[:getNumOfPagesOnFrontpage(posts)],
        Template:    t,
        Destination: destination,
        PageTitle:   "",
    }}

    …创建其他的生成器...

    generators = append(generators, &fg, &ag, &tg, &sg, &rg, &statg)

    for _, generator := range generators {
        wg.Add(1)
        go func(g Generator) {
            defer wg.Done()
            pool <- struct{}{}
            defer func() { <-pool }()
            if err := g.Generate(); err != nil {
                errors <- err
            }
        }(generator)
    }

    go func() {
        wg.Wait()
        close(finished)
    }()

    select {
    case <-finished:
        return nil
    case err := <-errors:
        if err != nil {
           return err
        }
    }
    return nil
}

runTasks 函数使用带缓冲的 channel,限制最多只能开启50个 goroutines,来创建所有生成器,将它们添加到一个 slice 中,然后并发运行。

这些例子只是在 Go 中编写静态站点生成器的基本概念的一个很短的片段。

如果您对完整的实现感兴趣,可以在此处找到代码

总结

写个人博客生成器是绝对的爆炸和伟大的学习实践。 使用我自己的工具创建我的博客也是非常令人满意的。

为了发布我的文章到 AWS,我还创建了 static-aws-deploy,这是另一个 Go命令行工具。

如果你想自己使用这个工具,只需要 fork repo 并更改配置。 但是,由于 Hugo 提供了所有这些和更多的功能,我没有花太多时间进行可定制性或可配置性。

当然,应该不要一直重新发明轮子,但是有时重新发明一两轮可能是有益的,可以帮助你在这个过程中学到很多东西。:)

英文原文

182017
 

Go 标准库有一个 expvar 包。 该软件包可以通过 JSON 格式的 HTTP API 公开您的应用程序和 Go 运行时的指标。 我认为这个软件包对于每个人 gopher 来说都是有用的。 但是,来自 godoc.org 的数据表明,没有多少人知道这个包。截止目前(2017-6-18),该包被公开的项目 import 2207 次,相比较而言,连 image 包被 import 3491 次之多。这篇文章,我想展示 expvar 包的工作原理,以及它的使用。

包简介

包 expvar 为公共变量提供了一个标准化的接口,如服务器中的操作计数器。它以 JSON 格式通过 /debug/vars 接口以 HTTP 的方式公开这些公共变量。

设置或修改这些公共变量的操作是原子的。

除了为程序增加 HTTP handler,此包还注册以下变量:

    cmdline   os.Args
    memstats  runtime.Memstats

导入该包有时只是为注册其 HTTP handler 和上述变量。 要以这种方式使用,请将此包通过如下形式引入到程序中:

    import _ "expvar"

例子

在浏览此包的详细信息之前,我想演示使用 expvar 包可以做什么。以下代码创建一个在监听 1818端口的 HTTP 服务器。每个请求 hander() 后,在向访问者发送响应消息之前增加计数器。

    package main

    import (
        "expvar"
        "fmt"
        "net/http"
    )

    var visits = expvar.NewInt("visits")

    func handler(w http.ResponseWriter, r *http.Request) {
        visits.Add(1)
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
    }

    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":1818", nil)
    }

导入时,expvar 包将为 http.DefaultServeMux 上的模式 “/debug /vars” 注册一个处理函数。此处理程序返回已在 expvar 包中注册的所有指标。运行代码并访问 http://localhost:1818/debug/vars,您将看到如下所示的内容。输出被截断以增加可读性:

    {
      "cmdline": [
        "/tmp/go-build872151671/command-line-arguments/_obj/exe/main"
      ],
      "memstats": {
        "Alloc": 397576,
        "TotalAlloc": 397576,
        "Sys": 3084288,
        "Lookups": 7,
        "Mallocs": 5119,
        "Frees": 167,
        "HeapAlloc": 397576,
        "HeapSys": 1769472,
        "HeapIdle": 1015808,
        "HeapInuse": 753664,
        "HeapReleased": 0,
        "HeapObjects": 4952,
        "StackInuse": 327680,
        "StackSys": 327680,
        "MSpanInuse": 14240,
        "MSpanSys": 16384,
        "MCacheInuse": 4800,
        "MCacheSys": 16384,
        "BuckHashSys": 2380,
        "GCSys": 131072,
        "OtherSys": 820916,
        "NextGC": 4194304,
        "LastGC": 0,
        "PauseTotalNs": 0,
        "PauseNs": [
          0,
          0,
        ],
        "PauseEnd": [
          0,
          0
        ],
        "GCCPUFraction": 0,
        "EnableGC": true,
        "DebugGC": false,
        "BySize": [
          {
            "Size": 16640,
            "Mallocs": 0,
            "Frees": 0
          },
          {
            "Size": 17664,
            "Mallocs": 0,
            "Frees": 0
          }
        ]
      },
      "visits": 0
    }

信息真不少。这是因为默认情况下该包注册了os.Args 和 runtime.Memstats 两个指标。 我想在这个 JSON 响应结束时关注访问计数器。 因为计数器还没有增加,它的值仍然为0。现在通过访问http:// localhost:1818/golang 来增加计数器,然后返回。计数器不再为0。

expvar.Publish

expvar 包相当小且容易理解。它主要由两个部分组成。第一个是函数 expvar.Publish(name string,v expvar.Var)。该函数可用于在未导出的全局注册表中注册具有特定名称的 v。以下代码段显示了具体实现。接下来的 3 个代码段是从 expvar 包的源代码中截取的。

    // Publish declares a named exported variable. This should be called from a
    // package's init function when it creates its Vars. If the name is already
    // registered then this will log.Panic.
    func Publish(name string, v Var) {
        mutex.Lock()
        defer mutex.Unlock()

        // Check if name has been taken already. If so, panic.
        if _, existing := vars[name]; existing {
            log.Panicln("Reuse of exported var name:", name)
        }

         // vars is the global registry. It is defined somewhere else in the
         // expvar package like this:
         //
         //  vars = make(map[string]Var)
        vars[name] = v
        // 一方面,该包中所有公共变量,放在 vars 中,同时,通过 varKeys 保存了所有变量名,并且按字母序排序,即实现了一个有序的、线程安全的哈希表
        varKeys = append(varKeys, name)
        sort.Strings(varKeys)
    }

expvar.Var

另一个重要的组件是 expvar.Var 接口。 这个接口只有一个方法:

    // Var is an abstract type for all exported variables.
    type Var interface {
            // String returns a valid JSON value for the variable.
            // Types with String methods that do not return valid JSON
            // (such as time.Time) must not be used as a Var.
            String() string
    }

所以你可以在有 String() string 方法的所有类型上调用 Publish() 函数。

expvar.Int

expvar 包附带了其他几个类型,它们实现了 expvar.Var 接口。其中一个是 expvar.Int,我们已经在演示代码中通过 expvar.NewInt(“visits”) 方式使用它了,它会创建一个新的 expvar.Int,并使用 expvar.Publish 注册它,然后返回一个指向新创建的 expvar.Int 的指针。

    func NewInt(name string) *Int {
        v := new(Int)
        Publish(name, v)
        return v
    }

expvar.Int 包装一个 int64,并有两个函数 Add(delta int64) 和 Set(value int64),它们以线程安全的方式修改整数。

Other types

除了expvar.Int,该包还提供了一些实现 expvar.Var 接口的其他类型:

* expvar.Float
* expvar.String
* expvar.Map
* expvar.Func

前两个类型包装了 float64 和 string。后两种类型需要稍微解释下。

expvar.Map 类型可用于使指标出现在某些名称空间下。我经常这样用:

    var stats = expvar.NewMap("tcp")
    var requests, requestsFailed expvar.Int

    func init() {
        stats.Set("requests", &requests)
        stats.Set("requests_failed", &requestsFailed)
    }

这段代码使用名称空间 tcp 注册了两个指标 requests 和 requests_failed。它将显示在 JSON 响应中,如下所示:

    {
        "tcp": {
            "request": 18,
            "requests_failed": 21
        }
    }

当要使用某个函数的执行结果时,您可以使用 expvar.Func。假设您希望计算应用程序的正常运行时间,每次有人访问 http://localhost:1818/debug/vars 时,都必须重新计算此值。

    var start = time.Now()

    func calculateUptime() interface {
        return time.Since(start).String()
    }

    expvar.Publish("uptime", expvar.Func(calculateUptime))

关于 Handler 函数

本文开始时提到,可以简单的导入 expvar 包,然后使用其副作用,导出 /debug/vars 模式。然而,如果我们使用了一些框架,并非使用 http.DefaultServeMux,而是框架自己定义的 Mux,这时直接导入使用副作用可能会不生效。这时我们可以按照使用的框架,定义自己的路径,比如也叫:/debug/vars,然后,这对应的处理程序中,有两种处理方式:

1)将处理直接交给 expvar.Handler,比如:

    handler := expvar.Handler()
    handler.ServeHTTP(w, req)

2)自己遍历 expvar 中的公共变量,构造输出,甚至可以过滤 expvar 默认提供的 cmdline 和 memstats,我们看下 expvarHandler 的源码就明白了:(通过 expvar.Do 函数来遍历)

    func expvarHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        fmt.Fprintf(w, "{\n")
        first := true
        Do(func(kv KeyValue) {
            if !first {
                fmt.Fprintf(w, ",\n")
            }
            first = false
            fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
        })
        fmt.Fprintf(w, "\n}\n")
    }

Go 语言中文网 就是用了第1种方式来处理。

总结

通过 expvar 包,使得展示应用程序指标非常容易。我几乎在我写的每个应用程序中使用它来展示一些指示应用程序运行状况的指标。InfluxDB 和 Grafana,加上一个自定义的聚合器,我可以很容易监控我的应用程序。

在英文 expvar in action 的基础上有增减。

062017
 

如何同时提高一个软件系统的可维护性(Maintainability)和可复用性(Reuseability)是面向对象的设计要解决的核心问题。

面向对象设计中,设计模式被提及的比较多,然而,大家对在设计模式背后、更深层的、更具有普遍性的、共同的思想原则提及却较少。但它们是比设计模式本身更加基本和单纯的设计思想。

面向对象的设计原则包括:“开-闭”原则(OCP)、里氏代换原则(LSP)、依赖倒转原则(DIP)、接口隔离原则(ISP)、合成/聚合复用原则(CARP)、迪米特法则(LoD)。

这些面向对象设计原则是提高软件兄的可维护性和可复用性的指导性原则。在有现成的设计模式的地方,设计模式就是这些设计原则在具体问题的体现;在没有现成的设计模式的地方,这些设计原则也一样适用,同样可以对系统设计发挥指导作用,并对新模式的研究提供向导。

1、设计目标

一个好的系统设计应该达到三个目标:

1)可扩展性(Extensibility)
新的功能可以很容易地加入到系统中

2)灵活性(Flexibility)
可以允许代码修改平稳地发生,而不会波及到很多其他的模块

3)可插入性(Pluggability)
可以很容易地将一个类抽出去,同时将另一个有同样接口的类加入进来

2、复用的重要性

复用的好处:1)较高的生产效率;2)较高的软件质量;3)恰当使用可改善系统的可维护性

3、传统的复用

1)代码的剪贴复用;2)算法的复用;3)数据结构的复用

4、面向对象设计的复用

对设计目标的支持。

1)系统的可扩展性是由“开-闭”原则、里氏代换原则、依赖倒转原则和组合/聚合复用原则所保证;

2)系统的灵活性是由“开-闭”原则、迪米特法则、接口隔离原则所保证;
每个模块相对于其他模块独立存在,并只保持与其他模块尽可能少的通信

3)系统的可插入性由“开-闭”原则、里氏代换原则、组合/聚合复用原以及依赖倒转原则保证

5、“开-闭”原则(Open-Closed Principle)

面向对象的可复用设计的第一块基石是“开-闭”原则。

“开-闭”原则讲的是:一个软件应当对扩展开放,对修改关闭。

也就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。换言之,应当可以再不必修改源代码的情况下改变这个模块的行为。

满足“开-闭”原则的设计可以给一个软件系统两个无可比拟的优越性:

1)通过扩展已有的系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性;
2)已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性。

怎么做到“开-闭”原则?

1)抽象化是关键。在抽象层定义好接口,抽象层预见了可能的扩展,使得抽象层不需修改,从而满足“对修改关闭”;同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了“开-闭“原则的第一条。

2)对可变性的封装原则:找到一个系统的可变因素,将之封装起来。
①对可变性的封装不应该散落在代码的很多角落,而应当被封装到一个对象里面。继承可以看做是一种封装变化的方法。
②一种可变性不应当与另一种可变性混合在一起。

6、里氏代换原则(Liskov Substitution Principle)

从“开-闭”原则中可以看出面向对象的重要原则是创建抽象化,并且从抽象化导出具体化。具体化可以给出不同的版本,每一个版本都给出不同的实现。

从抽象化到具体化的导出要使用继承关系和里氏代换原则。

里氏代换原则:一个系统如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能觉察出基类对象和子类对象的区别。反之不成立。

里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体体现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

7、依赖倒转原则(Dependence Inversion Principle)

要依赖于抽象,不要依赖于实现。抽象不应当依赖于细节;细节应当依赖于抽象;针对接口编程,不要针对实现编程。

实现“开-闭”原则的关键是抽象化,并且从抽象化导出具体化实现。“开-闭”原则是目标,而达到这一目标的手段是依赖倒转原则。换言之,要实现“开-闭”原则,就应当坚持依赖倒转原则。违反依赖倒转原则,就不可能达到“开-闭”原则的要求。

8、接口隔离原则(Interface Segregation Principle)

使用多个专门的接口比使用单一的总接口要好;应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。

9、合成/聚合复用原则(Composite/Aggregate Reuse Principle)

在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。

尽量使用合成/聚合,而不要使用继承。

10、迪米特法则(LoD),又叫 最少知识原则

一个对象应当对其他对象有尽可能少的了解;一个软件实体应当与尽可能少的其他实体发生相互作用。

如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

一个遵循迪米特法则设计出来的系统在功能需要扩展时,会相对更容易地做到对修改的关闭。也就是说,迪米特法则是一条通向“开-闭”原则的道路。

092016
 

本文介绍 Unix I/O 模型中的4个通用系统调用:open()、read()、write()和close() 的 Go 语言封装。

1、Linux 中 open 系统调用的定义

#include <sys/stat.h>
#include <fcntl.h>

int open(const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

2、Go 中 open 系统调用的封装

一般的,我们使用 os 标准库的 Open/Create 方法来间接调用 open 系统调用,跟踪代码,找到 Go 中 open 系统调用的封装:

func Open(path string, mode int, perm uint32) (fd int, err error) {
    return openat(_AT_FDCWD, path, mode|O_LARGEFILE, perm)
}

2.1 openat 又是什么呢?

从 2.6.16 (Go 支持的 Linux 版本是 2.6.23)开始,Linux 内核提供了一系列新的系统调用(以at结尾),它们在执行与传统系统调用相似任务的同时,还提供了一些附加功能,对某些程序非常有用,这些系统调用使用目录文件描述符来解释相对路径。

#define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */
#include <fcntl.h>

int openat(int dirfd, const char* pathname, int flags, … /* mode_t mode */);
                   Returns file descriptor on success, or -1 on error

可见,openat 系统调用和 open 类似,只是添加了一个 dirfd 参数,其作用如下:
1)如果 pathname 中为一相对路径名,那么对其解释则以打开文件描述符 dirfd 所指向的目录为参照点,而非进程的当前工作目录;
2)如果 pathname 中为一相对路径,且 dirfd 中所含为特殊值 AT_FDCWD(其值为-100),那么对 pathname 的解释则相对于进程当前工作目录,这时 openat 和 open 行为一致;
3)如果 pathname 中为绝对路径,那么将忽略 dirfd 参数;

在 Go 中,只要存在相应的 at 系统调用,都会使用它。

2.2 解读 Go 中的 openat

有上可知,Go 中的 Open 并非执行 open 系统调用,而是 openat 系统调用,行为和 open 一致。

func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) {
    var _p0 *byte
    // 根据要求,path 必须是 C 语言中的字符串,即以 NULL 结尾
    // BytePtrFromString 的作用就是返回一个指向 NULL 结尾的字节数组指针
    _p0, err = BytePtrFromString(path)
    if err != nil {
        return
    }
    // SYS_OPENAT openat 系统调用编号
    r0, _, e1 := Syscall6(SYS_OPENAT, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(flags), uintptr(mode), 0, 0)
    // 空操作,用于保证 _p0 存活
    use(unsafe.Pointer(_p0))
    fd = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

2.3 反过来解读 os.OpenFile 函数

OpenFile 函数有一个点需要注意,创建或打开文件时,自动加上了 O_CLOEXEC 标志,也就是执行 Exec 系统调用时该文件描述符不会被继承;

os.OpenFile 返回 os.File 类型的指针,通过 File.Fd() 可以获取到文件描述符;

3、read、write 和 close 系统调用

通过 open 系统调用的分析可以很容易的自己分析 read、write和close 系统调用。

说明一点:close 系统调用,企图关闭一个未打开的文件描述符或两次关闭同一个文件描述符,会返回错误。一般都不需要关心错误。

4、lseek 系统调用

Go 中的 os 包的 File.Seek 对应的系统调用是 lseek

5、其他文件 I/O 相关的系统调用

os 包中,File 类型中 ReadAt/WriteAt 对应的系统调用是 pread/pwrite

Truncate 对应 truncate 等;

几乎所有 Linux 文件相关系统调用,Go 都有封装;另外,通过上面 Open 的介绍,即使没有封装,自己封装也不是难事。

082016
 

一、系统调用概述

系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。Linux 内核以 C 语言语法 API 接口形式(头文件),提供有一系列服务供程序访问。可以通过 man 2 syscall 查看系统调用信息。

关于系统调用,需要注意以下几点:
1、系统调用将处理器从用户态切换到核心态,以便 CPU 访问受到保护的内核内存;
2、系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识;
3、每个系统调用可辅之以一套参数,对用户控件(进程虚拟地址控件)与内核空间之间(相互)传递的信息加以规范;

以C语言为例,执行系统调用时,幕后会历经诸多步骤。以 x86-32 平台为例,按时间发生顺序对这些步骤加以分析:
1、应用程序通过 C 语言函数库中的外壳(wrapper)函数,来发起系统调用;
2、对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器;
3、由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器 (%eax) 中;
4、外壳函数执行一条中断机器指令(int 0×80),引发处理器从用户态切换到核心态,并执行系统中断 0×80(十进制128)的中断矢量所之指向的代码;(2.6内核 和 glibc 2.3.2 以后的版本支持 sysenter 指令,进入内核速度更快);
5、为响应中断 0×80,内核会调用 system_call 例程(内核源码中 arch/i386/entry.S)来处理这次中断;
6、若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 errno,然后外壳函数会返回到调用程序,并同时返回一个整数值,以表明系统调用是否成功;

二、Go 语言封装的系统调用

Go 语言调用系统调用,并没有使用系统提供的 C 语言函数形式,而是自己封装了系统调用。以 AMD64 为例,Go 语言提供了如下调用系统调用的方式:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

其中,Syscall 和 RawSyscall 的区别如下:(以6结尾的一样)
从源码可以看出,Syscall 开始和结束,分别调用了 runtime 中的进入系统调用和退出系统调用的函数,这就说明,系统调用被 runtime 运行时(调度器)管理,系统调用可以在任何 goroutine 中执行;而 RawSyscall 并没有,因此它可能会阻塞,导致整个程序阻塞。我们应该总是使用 Syscall,RawSyscall 存在的意义是为那些永远不会阻塞的系统调用准备的,比如 Getpid。我们自己的程序需要时,应该用 Syscall。

Go 中 Syscall 的实现,在汇编文件 syscall/asm_linux_amd64.s 中:

// func Syscall(trap int64, a1, a2, a3 int64) (r1, r2, err int64);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

TEXT    ·Syscall(SB),NOSPLIT,$0-56
    CALL    runtime·entersyscall(SB)
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX    // syscall entry
    SYSCALL     // 进入内核,执行内核处理例程
    // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 转无符号,http://lxr.free-electrons.com/source/include/linux/err.h#L17
    CMPQ    AX, $0xfffffffffffff001        // 发生错误,r1=-1
    JLS    ok
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX             // 错误码,因为错误码是负数,这里转为正数
    MOVQ    AX, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET

相关参考文献
https://groups.google.com/forum/#!searchin/golang-nuts/Syscall$20RawSyscall/golang-nuts/y9lT_1loJj4/g4ZrYB2_80YJ
《The Linux Programming Interface —A Linux and UNIX System Programming Handbook》

142016
 

用过PHP的人都知道,PHP处理JSON数据那是相当方便,json_encode和json_decode两个函数搞定一切。那么在Go中该怎么处理JSON呢?

一、encoding/json标准库

学习 json 库应该先了解 Go 中的 struct tag、reflect等知识。

1、概述

json包实现了json对象的编解码,参见RFC 4627。Json对象和go类型的映射关系请参见Marshal和Unmarshal函数的文档。
参见”JSON and Go”获取本包的一个介绍:http://golang.org/doc/articles/json_and_go.html 这是官方的一篇博文。

2、核心函数和类型

对于函数和类型,我们关注经常使用的。

1)Marshal 和 Unmarshal

这两个是最常使用的函数,也就是 json 对象的编解码。这两个函数的文档很长,详细解释了 Go 类型和 json 对象的映射关系等。映射关系整理如下:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

详细的编码解码规则,文档上解释的很详细,这里说几个关键点:
①默认情况下,按照上面提到的映射进行解析;
②如果对象实现了 json.Marshaler/Unmarshaler 接口且不是 nil 指针,则调用对应的方法进行编解码;如果没有实现该接口,但实现了 encoding.TextMarshaler/TextUnmarshaler 接口,则调用该接口的相应方法进行编解码;
③struct 中通过 “json” tag 来控制相关编解码,后面通过示例说明;
④struct 的匿名字段,默认展开;可以通过指定 tag 来使其不展开;
⑤如果存在匿名字段,如果同级别有相同字段名,不会冲突,具体处理规则文档有说明;
⑥在解码时到struct时,会忽略多余或不存在的字段(包括不导出的),而不会报错;

另外注意,传递给 Unmarshal 的第二个参数必须是指针。

使用示例:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    type Book struct {
        Name  string
        Price float64 // `json:"price,string"`
    }
    var person = struct {
        Name string
        Age  int
        Book
    }{
        Name: "polaris",
        Age:  30,
        Book: Book{
            Price: 3.4,
            Name:  "Go语言",
        },
    }

    buf, _ := json.Marshal(person)
    fmt.Println(string(buf))

    // Output:{"Name":"polaris","Age":30,"Price":3.4}
    // Book 中的 Name 被忽略了
}

如果不希望内嵌类型展开,只需加上 tag:

var person = struct {
        Name string
        Age  int
        Book `json:"Book"`
}

有时候,比如之前是 PHP(弱类型语言) 写的,Age 的值很可能是 “Age”:”30” 这种形式,现在改为用 Go 实现,为了兼容;或者返回给客户端 Price 这样的浮点值,可能会涉及精度问题,客户端只是单纯的展示,返回浮点值的字符串即可。针对这样的情况,只需要加上这样的 tag:`json:”,string” 即可。这里逗号后面的“string”是 tag option。

如果想忽略某个字段,加上`json:”-”`;如果在值为空时忽略,加上
omitempty option,如:`json:”,omitempty”`

在解码时,优先匹配 struct 导出字段的 tag,之后是 Field,最后是 Field 的各种大小写不明感的形式,如 Name,能匹配 NAME/NAme等等。

2)MarshalIndent 函数

该函数的功能和 Marshal一致,只是格式化 json,方便人工阅读。如上面例子使用该函数,MarshalIndent(person, “”, “\t”) 输出如下:

{
    "Name": "polaris",
    "Age": 30,
    "Price": 3.4
}

3)Encoder 和 Decoder

有时候,我们可能从 Request 之类的输入流中直接读取 json 进行解析或将编码的 json 直接输出,为了方便,标准库为我们提供了 Decoder 和 Encoder 类型。它们分别通过一个 io.Reader 和 io.Writer 实例化,并从中读取数据或写数据。

通过阅读源码可以发现,Encoder.Encode/Decoder.Decode 和 Marshal/Unmarshal 实现大体是一样;有一些不同点:Decoder 有一个方法 UseNumber,它的作用:默认情况下,json 的 number 会映射为 Go 中的 float64,有时候,这会有些问题,比如:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})
err := json.Unmarshal(b, &person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(int))

我们希望 age 是 int,结果 panic 了:interface conversion: interface is float64, not int.

我们改为 Decoder.Decode(用上 UseNumber) 试试:

b := []byte(`{"Name":"polaris","Age":30,"Money":20.3}`)

var person = make(map[string]interface{})

decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err := decoder.Decode(&person)
if err != nil {
    log.Fatalln("json unmarshal error:", err)
}

age := person["Age"]
log.Println(age.(json.Number).Int64())

我们使用了 json.Number 类型。

4)RawMessage 类型

该类型的定义是 type RawMessage []byte,可见保存的是原始的 json 对象,它实现了 Marshaler 和 Unmarshaler 接口,能够延迟对 json 进行解码。使用示例可以参考 http://docs.studygolang.com/pkg/encoding/json/#RawMessage上的例子。

5)其他函数或类型不常用(如错误类型等),在此不赘述,可以直接查阅官方文档。

二、实际应用中的问题

当客户端和服务器通讯使用 json 这种数据格式时,我们一方面会解码客户端的 json 数据,另一方面,需要对数据进行 json 编码,发送给客户端。

一般的,服务器发送给客户端的 json 数据,是通过 struct、[]struct 或 map[string]interface{} 等编码得到。这里除了上文说到的,可能需要对数值类型使用 string tag options 之外,对于 time.Time 类型(实现了Marshaler 接口),默认编码得到的时间格式是:RFC3339即2006-01-02T15:04:05Z07:00,很多时候客户端可能不希望得到这样的时间,他们更多时候只是需要一个可读的时间字符串,如 2006-01-02 15:04:05。对此,我们可以定义自己的类型 type OftenTime time:

func (self OftenTime) MarshalJSON() ([]byte, error) {
    t := time.Time(self)
    if y := t.Year(); y < 0 || y >= 10000 {
        return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
    }
    return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil
}

func (this *OftenTime) UnmarshalJSON(data []byte) (err error) {
    t := time.Time(*this)
    return t.UnmarshalJSON(data)
}

另外,有一个坑,json 对象的 key 必须是字符串,所以 map[int]interface{} 在编码时会报错,错误是 json.UnsupportedTypeError.

对于接收客户端数据,进行 json 解码,遇到的问题可能比较多,特别是同时接收多种语言的数据,比如 PHP、Java 等。比如 b := []byte(`{“Name”:”polaris”,”Age”:30,”Money”:20.3}`),PHP 传递过来的可能是:b := []byte(`{“Name”:”polaris”,”Age”:”30″,”Money”:”20.3″}`),在使用 struct 接收数据时,对于 Age,如果是 int,我们可以直接定义为 int 类型,但如果是string,可以通过 string tag options 接收;但如果Age有时是 int, 有时是 string,就会出问题。最理想的情况,当然是不希望出现这种情况,但有一点,程序要保证出现这种情况时,不能 panic。

针对这种情况,有一种办法,是定义为 json.Number 类型。不过这样有两个问题:1)使用时不方便,需要调用 json.Number 的方法,而且它的方法返回值还得判断错误;2)编码 json.Number 时,是数值,如果客户端需要字符串,就不行了。

在实际应用中,我就遇到了上面的问题,于是,自己写了一个 json 解析,能支持自动类型转换。代码开源在 github:https://github.com/polaris1119/jsonutils

三、性能问题

很明显,json 的编解码,使用了 Go 的反射功能,所以,性能自然不是太好,正因为如此,有了 ffjson、easyjson 之类的开源库(在 github 上),它们的原理是通过 go generate 根据 struct 生成相应的代码,避免反射。如果你对性能要求比较高,但又不想使用msgpack/pb/thrift 之类的,那么可以考虑使用 ffjson/easyjson 来优化性能。

132015
 

微信支付获取 prepay id 偶尔失败问题总结。

微信支付会要求先从微信服务器获取 prepay id (https://api.mch.weixin.qq.com/pay/unifiedorder)。我们开发完成后(语言是PHP,使用微信的支付SDK,请求时使用curl),在测试环境的机器上,基本没有发现请求失败的情况,上线后,却发现经常出现错误,概率1/5甚至更高。开始没有深究原因,采用重试的方式,不过发现,只要失败了,重试也会失败。

记录下 curl 的错误是:errno:35, error: SSL Connect Error。

网上查相关资料,没有找到解决方案。联系微信技术支持,他们没有任何建议,觉得是我们的问题,让我们自己查。

在我们服务器上通过 tcpdump 抓包:tcpdump -i eth1 ip host 140.207.69.102 -w wxpay.cap,对比成功和失败的包(使用wireshark分析):

成功的数据包:

失败的数据包:

可以看到,失败的时候,TCP 3次握手后马上进行4次挥手操作 没有任何内容交互,而且 close 是客户端(我们这边)发起的。

推论:与remote服务端应该无关,还没开始https内容部分的交换,应该是本地在进行某些工作的时候发现异常,close了连接(但程序如果让我写会考虑先本地工作结束后再connect,不清楚为什么先connect然后再获取本地资源)。

可以通过 strace curl -X POST https://api.mch.weixin.qq.com/pay/unifiedorder 查看整个系统调用过程,是先 connect,然后从 系统本地etc目录下加载一些CA相关文件。

也可以通过 curl -v -X POST https://api.mch.weixin.qq.com/pay/unifiedorder 查看相关信息。对于 PHP 中,可以设置:curl_setopt($ch, CURLOPT_VERBOSE, true)

失败时是类似这样的输出:

* About to connect() to api.mch.weixin.qq.com port 443 (#0)
* Trying 140.207.69.102… connected
* Connected to api.mch.weixin.qq.com (140.207.69.102) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* NSS error -5990
* Closing connection #0
* SSL connect error
curl: (35) SSL connect error

成功时输出如下(省略了response):

* About to connect() to api.mch.weixin.qq.com port 443 (#0)
* Trying 140.207.69.102… connected
* Connected to api.mch.weixin.qq.com (140.207.69.102) port 443 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* NSS: client certificate not found (nickname not specified)
* SSL connection using TLS_DHE_RSA_WITH_AES_256_CBC_SHA
* Server certificate:
* subject: CN=payapp.weixin.qq.com,OU=R&D,O=Tencent Technology (Shenzhen) Company Limited,L=shenzhen,ST=guangdong,C=CN
* start date: 4月 28 00:00:00 2015 GMT
* expire date: 4月 27 23:59:59 2016 GMT
* common name: payapp.weixin.qq.com
* issuer: CN=GeoTrust SSL CA – G2,O=GeoTrust Inc.,C=US
> POST /pay/unifiedorder HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.16.2.3 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
> Host: api.mch.weixin.qq.com
> Accept: */*

在网上找到了一个帖子: http://serverfault.com/questions/606135/curl-35-ssl-connect-error, 问题和我们遇到的一样,虽然操作系统版本和NSS版本不一样。按上面的提示,升级 nss(Mozilla Network Security Services 网络安全服务):yum update nss, 然后重启 php,问题解决。

总结:
1、从现象:有些机器有问题,有些没有问题;有些概率大,有些概率小;可大体上推断,跟机器有关系,可能机器环境不一样导致的;
2、抓包分析原因,进一步确认不是服务端(微信支付)的问题;
3、根据错误描述在网络上寻求帮助;
4、学习 curl 相关选项的使用;strace 的使用;