标签 errcheck 下的文章

Go语言项目的安全评估技术

在今年夏天我们对Kubernetes的评估成功之后,我们收到了大量Go项目的安全评估需求。为此,我们将在其他编译语言中使用过的安全评估技术和策略调整适配到多个Go项目中。

我们从了解语言的设计开始,识别出开发人员可能无法完全理解语言语义特性的地方。多数这些被误解的语义来自我们向客户报告的调查结果以及对语言本身的独立研究。尽管不是详尽无遗,但其中一些问题领域包括作用域、协程、错误处理和依赖管理。值得注意的是,其中许多与运行时没有直接关系。默认情况下,Go运行时本身的设计是安全的,避免了很多类似C语言的漏洞。

对根本原因有了更好地理解后,我们搜索了现有的能帮助我们快速有效检测客户端代码库的工具。结果我们找到一些静态和动态开源工具,其中包括了一些与Go无关的工具。为了配合这些工具使用,我们还确定了几种有助于检测的编译器配置。

一. 静态分析

由于Go是一种编译型语言,因此编译器在生成二进制可执行文件之前就检测并杜绝了许多潜在的错误模式。虽然对于新的Go开发人员来说,这些编译器的输出比较烦,但是这些警告对于防止意外行为以及保持代码的清洁和可读性非常重要。

静态分析趋向于捕获很多未包括在编译器错误和警告中的悬而未决的问题。在Go语言生态系统中,有许多不同的工具,例如go-vetstaticcheckanalysis包中的工具。这些工具通常会识别出诸如变量遮蔽、不安全的指针使用以及未使用的函数返回值之类的问题。调查这些工具显示警告的项目区域通常会发现可被利用(进行安全攻击)的功能特性。

这些工具绝不是完美的。例如,go-vet可能会错过非常常见的问题,例如下面例子中这种。

package main

import "fmt"

func A() (bool, error) { return false, fmt.Errorf("I get overridden!") }

func B() (bool, error) { return true, nil }

func main() {
    aSuccess, err := A()
    bSuccess, err := B()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(aSuccess, ":", bSuccess)
}

这个例子未使用A函数的err返回值,并在表达式左侧为bSuccess赋值期间立即重新对err做了赋值。编译器针对这种情况不会提供警告,而go-vet也不会检测到该问题;errcheck也不会。实际上,能成功识别这种情况的工具是前面提到的staticcheck和ineffassign,它们将A的错误返回值标识为未使用或无效。

示例程序的输出以及errcheck,go-vet,staticcheck和ineffassign的检查结果如下:

$ go run .
false : true
$ errcheck .
$ go vet .
$ staticcheck .
main.go:5:50: error strings should not be capitalized (ST1005)
main.go:5:50: error strings should not end with punctuation or a newline (ST1005)
main.go:10:12: this value of err is never used (SA4006)
$ ineffassign .
main.go:10:12: ineffectual assignment to err

当您深入研究此示例时,您可能会想知道为什么编译器没有针对此问题发出警告。当程序中有未使用的变量时,Go编译器将出错,但此示例成功通过编译。这是由“短变量声明”的语义引起的。下面是短变量声明的语法规范:

ShortVarDecl = IdentifierList ":=" ExpressionList .

根据规范,短变量声明具有重新声明变量的特殊功能,只要:

  • 重新声明在多变量短声明中。
  • 重新声明的变量在同一代码块或函数的参数列表中声明较早。
  • 重新声明的变量与先前的声明具有相同的类型。
  • 声明中至少有一个非空白变量(“_”)是新变量。

所有这些约束在上一个示例中均得到满足,从而防止了编译器针对此问题产生编译错误。

许多工具都具有类似这样的极端情况,即它们在识别相关问题或识别问题但以不同的方式描述时均未成功。使问题复杂化的是,这些工具通常需要先构建Go源代码,然后才能执行分析。如果分析人员无法轻松构建代码库或其依赖项,这将使第三方安全评估变得复杂。

尽管存在这些困难,但只要付出一点点努力,这些工具就可以很好地提示我们在项目中从何处查找问题。我们建议至少使用gosecgo-vetstaticcheck。对大多数代码库而言,这些工具具有良好的文档和人机工效。他们还提供了针对常见问题的多种检查(例如ineffassign或errcheck)。但是,要对特定类型的问题进行更深入的分析,可能必须使用更具体的分析器,直接针对SSA开发定制的工具或使用semmle

二. 动态分析

一旦执行了静态分析并检查了结果,动态分析技术通常是获得更深层结果的下一步。由于Go的内存安全性,动态分析通常发现的问题是导致硬崩溃(hard crash)或程序状态无效的问题。Go社区已经建立了各种工具和方法来帮助识别Go生态系统中这些类型的问题。此外,可以改造现有的与语言无关的工具以满足Go动态分析的需求,我们将在下面展示。

1. 模糊测试

Go语言领域中最著名的动态测试工具可能是Dimitry Vyukovgo-fuzz了。该工具使您可以快速有效地实施模糊测试,并且它已经有了不错的战利品。更高级的用户在猎错过程中可能还会发现分布式的模糊测试libFuzzer的支持非常有用。

Google还发布了一个更原生的模糊器(fuzzer),它拥有一个与上面的go-fuzz相似的名字:gofuzz。它通过初始化具有随机值的结构来帮助用户。与Dimitry的go-fuzz不同,Google的gofuzz不会生成夹具(harness)或协助提供存储崩溃时的输出信息、模糊输入或任何其他类型的信息。尽管这对于测试某些目标可能是不利的,但它使轻量级且可扩展的框架成为可能。

为了简洁起见,我们请您参考各自自述文件中这两个工具的示例。

2. 属性测试(property test)

译注:属性测试指编写对你的代码来说为真的逻辑语句(即“属性”),然后使用自动化工具来生成测试输入(一般来说,是指某种特定类型的随机生成输入数据),并观察程序接受该输入时属性是否保持不变。如果某个输入违反了某一条属性,则证明用户程序存在错误 – 摘自网络。

与传统的模糊测试方法不同,Go的testing包(通常用于单元测试和集成测试)为Go函数的“黑盒测试” 提供了testing/quick子包。换句话说,它提供了属性测试的基本原语。给定一个函数和生成器,该包可用于构建夹具,以测试在给定输入生成器范围的情况下潜在的属性违规。以下示例是直接摘自官方文档。

func TestOddMultipleOfThree(t *testing.T) {
    f := func(x int) bool {
        y := OddMultipleOfThree(x)
        return y%2 == 1 && y%3 == 0
    }
    if err := quick.Check(f, nil); err != nil {
        t.Error(err)
    }
}

上面示例正在测试OddMultipleOfThree函数,其返回值应始终为3的奇数倍。如果不是,则f函数将返回false并将违反该属性。这是由quick.Check功能检测到的。

虽然此包提供的功能对于属性测试的简单应用是可以接受的,但重要的属性通常不能很好地适合这种基本界面。为了解决这些缺点,诞生了leanovate/gopter框架。Gopter为常见的Go类型提供了各种各样的生成器,并且支持您创建与Gopter兼容的自定义生成器。通过gopter/commands子包还支持状态测试,这对于测试跨操作序列的属性是否有用很有有帮助。除此之外,当违反属性时,Gopter会缩小生成的输入。请参阅下面的输出中输入收缩的属性测试的简要示例。

Compute结构的测试夹具:

package main_test
import (
  "github.com/leanovate/gopter"
  "github.com/leanovate/gopter/gen"
  "github.com/leanovate/gopter/prop"
  "math"
  "testing"
)

type Compute struct {
  A uint32
  B uint32
}

func (c *Compute) CoerceInt () { c.A = c.A % 10; c.B = c.B % 10; }
func (c Compute) Add () uint32 { return c.A + c.B }
func (c Compute) Subtract () uint32 { return c.A - c.B }
func (c Compute) Divide () uint32 { return c.A / c.B }
func (c Compute) Multiply () uint32 { return c.A * c.B }

func TestCompute(t *testing.T) {
  parameters := gopter.DefaultTestParameters()
  parameters.Rng.Seed(1234) // Just for this example to generate reproducible results

  properties := gopter.NewProperties(parameters)

  properties.Property("Add should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Add()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Subtract should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Subtract()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Multiply should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Multiply()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Divide should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Divide()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.TestingRun(t)
}

执行测试夹具并观察属性测试的输出(除法失败):

user@host:~/Desktop/gopter_math$ go test
+ Add should never fail.: OK, passed 100 tests.
Elapsed time: 253.291µs
+ Subtract should never fail.: OK, passed 100 tests.
Elapsed time: 203.55µs
+ Multiply should never fail.: OK, passed 100 tests.
Elapsed time: 203.464µs
! Divide should never fail.: Error on property evaluation after 1 passed
   tests: Check paniced: runtime error: integer divide by zero
goroutine 5 [running]:
runtime/debug.Stack(0x5583a0, 0xc0000ccd80, 0xc00009d580)
    /usr/lib/go-1.12/src/runtime/debug/stack.go:24 +0x9d
github.com/leanovate/gopter/prop.checkConditionFunc.func2.1(0xc00009d9c0)
    /home/user/go/src/github.com/leanovate/gopter/prop/check_condition_func.g
  o:43 +0xeb
panic(0x554480, 0x6aa440)
    /usr/lib/go-1.12/src/runtime/panic.go:522 +0x1b5
_/home/user/Desktop/gopter_math_test.Compute.Divide(...)
    /home/user/Desktop/gopter_math/main_test.go:18
_/home/user/Desktop/gopter_math_test.TestCompute.func4(0x0, 0x0)
    /home/user/Desktop/gopter_math/main_test.go:63 +0x3d
# snip for brevity;

ARG_0: 0
ARG_0_ORIGINAL (1 shrinks): 117380812
ARG_1: 0
ARG_1_ORIGINAL (1 shrinks): 3287875120
Elapsed time: 183.113µs
--- FAIL: TestCompute (0.00s)
    properties.go:57: failed with initial seed: 1568637945819043624
FAIL
exit status 1
FAIL    _/home/user/Desktop/gopter_math 0.004s

3. 故障注入

在攻击Go系统时,故障注入令人惊讶地有效。我们使用此方法发现的最常见错误包括对error类型的处理。因为error在Go中只是一种类型,所以当它返回时,它不会像panic语句那样自行改变程序的执行流程。我们通过强制生成来自最低级别(内核)的错误来识别此类错误。由于Go会生成静态二进制文件,因此必须在不使用LD_PRELOAD的情况下注入故障。我们的工具之一KRF使我们能够做到这一点。

在我们最近的Kubernetes代码库评估中,我们使用KRF找到了一个vendored依赖深处的问题,只需通过随机为进程和其子进程发起的read和write系统调用制造故障。该技术对通常与底层系统交互的Kubelet十分有效。该错误是在ionice命令出现错误时触发的,未向STDOUT输出信息并向STDERR发送错误。记录错误后,将继续执行而不是将STDERR的错误返回给调用方。这导致STDOUT后续被索引,从而导致索引超出范围导致运行时panic。

下面是导致kubelet panic的调用栈信息:

E0320 19:31:54.493854    6450 fs.go:591] Failed to read from stdout for cmd [ionice -c3 nice -n 19 du -s /var/lib/docker/overlay2/bbfc9596c0b12fb31c70db5ffdb78f47af303247bea7b93eee2cbf9062e307d8/diff] - read |0: bad file descriptor
panic: runtime error: index out of range

goroutine 289 [running]:
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.GetDirDiskUsage(0xc001192c60, 0x5e, 0x1bf08eb000, 0x1, 0x0, 0xc0011a7188)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:600 +0xa86
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.(*RealFsInfo).GetDirDiskUsage(0xc000bdbb60, 0xc001192c60, 0x5e, 0x1bf08eb000, 0x0, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:565 +0x89
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).update(0xc000ee7560, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:82 +0x36a
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).trackUsage(0xc000ee7560)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:120 +0x13b
created by
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).Start
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:142 +0x3f

下面例子:记录了STDERR日志但未将error返回调用方。

stdoutb, souterr := ioutil.ReadAll(stdoutp)
if souterr != nil {
    klog.Errorf("Failed to read from stdout for cmd %v - %v", cmd.Args, souterr)
}

当stdout为空,也尝试使用索引,这是运行时出现panic的原因:

usageInKb, err := strconv.ParseUint(strings.Fields(stdout)[0], 10, 64)

更完整的包含重现上述问题的步骤,可参见我们的Kubernetes最终报告附录G(第109页),那里详细介绍了针对Kubelet使用KRF的方法。

Go的编译器还允许将测量工具包含在二进制文件中,从而可以在运行时检测race状况,这对于将潜在的race识别为攻击者非常有用,但也可以用来识别对defer、panic和recover的不正确处理。我们构建了Trailofbits/on-edge来做到这一点:识别函数入口点和函数panic点之间的全局状态变化,并通过Go race检测器”泄露”此信息。有关OnEdge的更多详细信息,请参见我们以前的博客文章“在Go中选择正确panic的方式”

实践中,我们建议使用:

  • dvyukov/go-fuzz为组件解析输入建立夹具
  • google/gofuzz用于测试结构验证
  • leanovate/gopter用于增强现有的单元和集成测试以及测试规范的正确性
  • Trailofbits/krf和Trailofbits/on-edge用于测试错误处理。

除KRF外,所有这些工具在实践中都需要付出一些努力。

三. 利用编译器的优势

Go编译器具有许多内置功能和指令(directive),可帮助我们查找错误。这些功能隐藏在各种开关中中,并且需要一些配置才能达到我们的目的。

1. 颠覆类型系统

有时在尝试测试系统功能时,导出函数不是我们要测试的。要获得对所需的函数的测试访问权,可能需要重命名许多函数,以便可以将其导出,这可能会很麻烦。要解决此问题,可以使用编译器的build指令(directive)进行名称链接(name linking)以及导出系统的访问控制。作为此功能的示例,下面的程序(从Stack Overflow答案中提取)访问未导出的reflect.typelinks函数,并随后迭代类型链接表以识别已编译程序中存在的类型。

下面是使用linkname build directive的Stack Overflow答案的通用版本:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
    return typelinks()
}

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return add(p, x, whySafe)
}

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

func main() {
    sections, offsets := Typelinks()
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := Add(base, uintptr(offset), "")
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
            fmt.Println(typ)
        }
    }
}

下面是typelinks表的输出:

$ go run main.go
**reflect.rtype
**runtime._defer
**runtime._type
**runtime.funcval
**runtime.g
**runtime.hchan
**runtime.heapArena
**runtime.itab
**runtime.mcache
**runtime.moduledata
**runtime.mspan
**runtime.notInHeap
**runtime.p
**runtime.special
**runtime.sudog
**runtime.treapNode
**sync.entry
**sync.poolChainElt
**syscall.Dirent
**uint8

如果需要在运行时进行更精细的控制(即,不仅仅是linkname指令),则可以编写Go的中间汇编码,并在编译过程中包括它。尽管在某些地方它可能不完整且有些过时,但是teh-cmc/go-internals提供了有关Go如何组装函数的很好的介绍。

2. 编译器生成的覆盖图

为了帮助进行测试,Go编译器可以执行预处理以生成coverage信息。这旨在标识单元测试和集成测试的测试覆盖范围信息,但是我们也可以使用它来标识由模糊测试和属性测试生成的测试覆盖范围。Filippo Valsorda在博客文章中提供了一个简单的示例。

3. 类型宽度安全

Go支持根据目标平台自动确定整数和浮点数的大小。但是,它也允许使用固定宽度的定义,例如int32和int64。当混合使用自动宽度和固定宽度大小时,对于跨多个目标平台的行为,可能会出现错误的假设。

针对目标的32位和64位平台构建进行测试将有助于识别特定于平台的问题。这些问题通常在执行验证、解码或类型转换的时候发现,原因在于对源和目标类型属性做出了不正确的假设。在Kubernetes安全评估中就有一些这样的示例,特别是TOB-K8S-015:使用strconv.Atoi并将结果向下转换时的溢出(Kubernetes最终报告中的第42页),下面是这个示例。

// updatePodContainers updates PodSpec.Containers.Ports with passed parameters.
func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) {
    port := -1
    hostPort := -1
    if len(params["port"]) > 0 {
        port, err = strconv.Atoi(params["port"]) // <-- this should parse port as strconv.ParseUint(params["port"], 10, 16)
        if err != nil {
            return err
        }
    }
       // (...)
    // Don't include the port if it was not specified.
    if len(params["port"]) > 0 {
        podSpec.Containers[0].Ports = []v1.ContainerPort{
            {
                ContainerPort: int32(port), // <-- this should later just be uint16(port)
            },
        }

错误的类型宽度假设导致的溢出:

root@k8s-1:/home/vagrant# kubectl expose deployment nginx-deployment --port 4294967377 --target-port 4294967376
E0402 09:25:31.888983    3625 intstr.go:61] value: 4294967376 overflows int32
goroutine 1 [running]:
runtime/debug.Stack(0xc000e54eb8, 0xc4f1e9b8, 0xa3ce32e2a3d43b34)
    /usr/local/go/src/runtime/debug/stack.go:24 +0xa7
k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/util/intstr.FromInt(0x100000050, 0xa, 0x100000050, 0x0, 0x0)
...
service/nginx-deployment exposed

实际上,很少需要颠覆类型系统。最需要的测试目标已经是导出了的,可以通过import获得。我们建议仅在需要助手和测试类似的未导出函数时才使用此功能。至于测试类型宽度安全性,我们建议您尽可能对所有目标进行编译,即使没有直接支持也是如此,因为不同目标上的问题可能更明显。最后,我们建议至少生成包含单元测试和集成测试的项目的覆盖率报告。它有助于确定未经直接测试的区域,这些区域可以优先进行审查。

四. 有关依赖的说明

在诸如JavaScript和Rust的语言中,依赖项管理器内置了对依赖项审核的支持-扫描项目依赖项以查找已知存在漏洞的版本。在Go中,不存在这样的工具,至少没有处于公开可用且非实验状态的。

这种缺乏可能是由于存在多种不同的依赖关系管理方法:go-modgo-getvendored等。这些不同的方法使用根本不同的实现方案,导致无法直接识别依赖关系及其版本。此外,在某些情况下,开发人员通常会随后修改其vendor的依赖的源代码。

在Go的开发过程中,依赖管理问题的解决已经取得了进展,大多数开发人员都在朝使用go mod的方向发展。这样就可以通过项目中的go.mod跟踪和依赖项并进行版本控制,从而为以后的依赖项扫描工作打开了大门。我们可以在OWASP DependencyCheck工具中看到此类工作的示例,该工具是具有实验性质的go mod插件。

五. 结论

最终,Go生态系统中有许多可以使用的工具。尽管大多数情况是完全不同的,但是各种静态分析工具可帮助识别给定项目中的“悬而未决的问题”。当寻求更深层次的关注时,可以使用模糊测试,属性测试和故障注入工具。编译器配置随后增强了动态技术,使构建测试夹具和评估其有效性变得更加容易。

本文翻译自“Security assessment techniques for Go projects”


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Golang开发环境搭建-Vim篇

虽说sublimetext3+gosublime+gocode是目前较为 流行的Golang开发环境组合,但作为一名VIMer,没有一套得心应手的Vim for Golang dev心里总是过不去的。Golang虽然年轻,但即便是从Go 1版本发布(2012年3月28日)算起,掐指算来也有小三年了。全世界的开发者已经为Golang贡献了较为成熟的Vim插件了。有了这些插件,搭建出 一套高效的Golang开发环境还是不难的,网上也有大量的资料可以参考,其中就有vim-go作者自己发表的一篇文章《Go development environment for Vim》。不过看别人 写的与自己搭建体验的还是有大不同的,于是想来想去还是把整个过程记录下来。

一、一个干净的环境

找个干净的基础环境,方便确认每个搭建步骤后的效果:

Ubuntu 14.04 x86_64
vim version 7.4.52
go version go1.4beta1 linux/amd64

再准备一个编辑Go源码的测试源文件:

//hellogolang.go

package main

import "fmt"

func main() {
        fmt.Println("Hello Golang!")
}

用于验证每个搭建步骤后的变化。

二、严格按照vim-go的官方说明逐一搭建

Vim-go是当前使用最为广泛的用于搭建Golang开发环境的vim插件,这里我同样使用vim-go作为核心和基础进行环境搭建的。vim-go利 用开源Vim插件管理器安装,gmarik/Vundle.vim是目前被推荐次数更多的Vim插件管理器,超过了pathogen。这里我们 就用vundle来作为Vim的插件管理工具。

1、安装Vundle.vim

Vundle.vim的安装步骤如下:

mkdir ~/.vim/bundle
git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim   
                                                                  

创建~/.vimrc文件(如果你没有这个文件的话),在文件顶部添加有关Vundle.vim的配置:

set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

此时Vim仅安装了Vundle.vim这一个插件。编辑hellogolang.go时与编辑普通文本文件无异,一切都还是Vim的默认属性。

2、安装Vim-go

编辑~/.vimrc,在vundle#beginvundle#end间增加一行:

Plugin 'fatih/vim-go'

在Vim内执行 :P luginInstall

Vundle.vim会在左侧打开一个Vundle Installer Preview子窗口,窗口下方会提示:“Processing 'fatih/vim-go'”,待安装完毕后,提示信息变 成“Done!”。

这时,我们可以看到.vim/bundle下多了一个vim-go文件夹:

$ ls .vim/bundle/
vim-go/  Vundle.vim/

此时,再次编辑hellogolang.go,语法高亮有了, 保存时自动format(利用$GOBIN/gofmt)也有了,但其他高级功能,比如自动import缺失的 package、自动补齐仍然没有,我们还要继续安装一些东东。

3、安装go.tools Binaries

vim-go安装说明中提到所有必要的binary需要先安装好,比如gocode、godef、goimports等。

通过:GoInstallBinaries,这些vim-go依赖的二进制工具将会自动被下载,并被安装到$GOBIN下或$GOPATH/bin下。(这个工具需要依赖git或hg,需要提前安装到你的OS中。)

:GoInstallBinaries的执行是交互式的,你需要回车确认:

vim-go: gocode not found. Installing github.com/nsf/gocode to folder /home/tonybai/go/bin
vim-go: goimports not found. Installing code.google.com/p/go.tools/cmd/goimports to folder /home/tonybai/go/bin/
vim-go: godef not found. Installing code.google.com/p/rog-go/exp/cmd/godef to folder /home/tonybai/go/bin/
vim-go: oracle not found. Installing code.google.com/p/go.tools/cmd/oracle to folder /home/tonybai/go/bin/
vim-go: gorename not found. Installing code.google.com/p/go.tools/cmd/gorename to folder /home/tonybai/go/bin/
vim-go: golint not found. Installing github.com/golang/lint/golint to folder /home/tonybai/go/bin/

vim-go: errcheck not found. Installing github.com/kisielk/errcheck to folder /home/tonybai/go/bin/

不过这些代码多在code.google.com上托管,因此由于众所周知的原因,vim-go的自动安装很可能以失败告终,这样就需要你根据上 面日志中提到的各个工具的源码地址逐一去下载并本地安装。无法搭梯子的,可以通过http://gopm.io 下载相关包。

安装后,$GOBIN下的新增Binaries如下:
-rwxr-xr-x  1 tonybai tonybai  5735552 11??  7 11:03 errcheck*
-rwxr-xr-x  1 tonybai tonybai  9951008 11??  7 10:33 gocode*
-rwxr-xr-x  1 tonybai tonybai  5742800 11??  7 11:07 godef*
-rwxr-xr-x  1 tonybai tonybai  4994120 11??  7 11:00 goimports*
-rwxr-xr-x  1 tonybai tonybai  5750152 11??  7 11:03 golint*
-rwxr-xr-x  1 tonybai tonybai  6381832 11??  7 11:01 gorename*
-rwxr-xr-x  1 tonybai tonybai  2954392 11??  7 10:38 gotags*
-rwxr-xr-x  1 tonybai tonybai  9222856 11??  7 11:01 oracle*

安装好这些Binaries后,我们来看看哪些特性被支持了。

再次编辑hellogolang.go

         - 新起一行输入fmt.,然后ctrl+x, ctrl+o,Vim 会弹出补齐提示下拉框,不过并非实时跟随的那种补齐,这个补齐是由gocode提供的。
    – 输入一行代码:time.Sleep(time.Second),执行:GoImports,Vim会自动导入time包。
    – 将光标移到Sleep函数上,执行:GoDef或命令模式下敲入gd,Vim会打开$GOROOT/src/time/sleep.go中 的Sleep函数的定义。执行:b 1返回到hellogolang.go。
    – 执行:GoLint,运行golint在当前Go源文件上。
    – 执行:GoDoc,打开当前光标对应符号的Go文档。
    – 执行:GoVet,在当前目录下运行go vet在当前Go源文件上。
    – 执行:GoRun,编译运行当前main package。
    – 执行:GoBuild,编译当前包,这取决于你的源文件,GoBuild不产生结果文件。
    – 执行:GoInstall,安装当前包。
    – 执行:GoTest,测试你当前路径下地_test.go文件。
    – 执行:GoCoverage,创建一个测试覆盖结果文件,并打开浏览器展示当前包的情况。
    – 执行:GoErrCheck,检查当前包种可能的未捕获的errors。
    – 执行:GoFiles,显示当前包对应的源文件列表。
    – 执行:GoDeps,显示当前包的依赖包列表。
    – 执行:GoImplements,显示当前类型实现的interface列表。
    – 执行:GoRename [to],将当前光标下的符号替换为[to]。

三、其他插件

到目前为止,我们还有若干特性没能实现,重点是:

    – 实时跟随的代码补齐
    – Code Snippet support

1、安装YCM(Your Complete Me)

在~/.vimrc中添加一行:

Plugin 'Valloric/YouCompleteMe'

保存退出后,再打开~/.vimrc并执行 :P luginInstall

安装完后,下面的提示栏提示:

ycm_client_support.[so|pyd|dll] and ycm_core.[so|pyd|dll] not detected; you need to compile YCM before using it. Read the docs!

似乎YCM是用了C++编写的模块对性能进行优化了,于是需要手工编译YCM的support库。步骤如下:

sudo apt-get install build-essential cmake python-dev
cd ~/.vim/bundle/YouCompleteMe
./install.sh

构建(编译C++很慢,需要耐心的等一会)ok后,再打开hellogolang.go,逐字的实时补全功能就具备了!Cool!

2、安装 UltiSnips

Vim-go默认是用ultisnips引擎插件,但这个插件需要单独安装。

同样,我们利用vundle来安装它,在~/.vimrc中添加一行:

Plugin 'SirVer/ultisnips'

snippet和snippet引擎是分开的。ultisnips是引擎,vim-go的go snippet定义在这里

https://github.com/fatih/vim-go/blob/master/gosnippets/snippets/go.snip

编辑hellogolang.go,按照go.snip中的说明,我们输入func后敲击tab键,我们发现期待的:

func name(params) type {
       
}

并没有出现。反倒是YCM的下拉提示显示在那里让你选择。似乎是ultisnips和YCM的键组合冲突了。ultisnips官方说明也的确如 此。ultisnips默认是用Tab展开snippet的,而YCM中的Tab用来选择补齐项,我们可以通过设置来避免这些。

我们在.vimrc中添加如下setting:

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips setting
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

这样让YCM通过回车和向下的箭头来做list item正向选择,通过向上箭头做反向选择。通过ctrl+space来原地触发补齐提示。

而ultisnips则是用tab做snippet展开,ctrl+b正向切换占位符,ctrl+z反向切换占位符。

3、安装molokai theme

Molokai theme是TextMate的theme的vim port,看着截图挺不错的,于是也安装了一下。

    mkdir ~/.vim/colors
    下载或copy https://github.com /fatih/molokai/blob/master/colors/molokai.vim到~/.vim /colors目录下
    在.vimrc添加一行:colorscheme molokai

四、.vimrc

前面讲到了vim-go有许多命令,在:xx模式下执行多显不便,于是你可以定义一些Mappings,比如:

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

这样我们在命令模式下,输入<,>+<r>就是运行 当前main包,以此类推。

另外下面这个配置使得我们在save file时既可以格式化代码,又可以自动插入包导入语句(或删除不用的包导入语句)。

" vim-go settings
let g:go_fmt_command = "goimports"

到这里,我们的Vim Golang开发环境就基本搭建好了。snippet+实时补齐让你Coding如飞!

五、附录:.vimrc文件

下面是截至目前为止全量.vimrc文件的内容:

set nocompatible              " be iMproved, required
filetype off                  " required
colorscheme molokai

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'gmarik/Vundle.vim'
Plugin 'fatih/vim-go'
Plugin 'Valloric/YouCompleteMe'

Plugin 'SirVer/ultisnips'

" All of your Plugins must be added before the following line
call vundle#end()            " required
filetype plugin indent on    " required

" set mapleader
let mapleader = ","

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)

" vim-go settings
let g:go_fmt_command = "goimports"

" YCM settings
let g:ycm_key_list_select_completion = ['', '']
let g:ycm_key_list_previous_completion = ['', '']
let g:ycm_key_invoke_completion = '<C-Space>'

" UltiSnips settings
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"

六、Mac OS X下Vim配置

1、MacVim替换

Mac OS X下的配置方法稍有不同,因为Mac下系统自带的Vim是7.3版本,YCM要求Vim 7.3.584+版本,因此我们需要安装MacVim以替代自带的Vim,目前MacVim最新版本是version 7.4.258,完全满足要求。在这里https://github.com/b4winckler/macvim/releases可以下载到最新的MacVim,下载后的MacVim可以通过如下步骤替换原Vim。

原Vim安装到/usr/bin/vim下。

MacVim解压后如下:

[tony@tonydeair ~/Downloads/MacVim-snapshot-73]$ls
MacVim.app/    README.txt    mvim*

我们执行以下步骤即可完成vim替换工作:

sudo mv /usr/bin/vim /usr/bin/vim.bak //备份一下原vim
cp mvim /usr/local/bin/
sudo ln -s /usr/local/bin/mvim /usr/bin/vim

2、插件安装和配置

按照上面Linux Vim的插件安装步骤和配置方法我们来配置MacVim,配置后,我们发现除了molokai的colorscheme没有生效外,其余插件工作均正常。而所有.go文件打开,均无molokai方案的颜色高亮,甚至连一般的颜色高亮都没有了。经过不断调试,发现了一个解决方法,在~/.vimrc中添加几行代码即可:

syntax on
au BufRead,BufNewFile *.go set filetype=go
colorscheme molokai

但这几行配置代码如果放在~/.vimrc的前面,则UltiSnips会无法工作,我将其移到~/.vimrc文件的末尾,这样就不存在冲突了(看来.vimrc的插件配置的先后顺序会对插件功能的正常使用有影响)。漂亮的molokai colorscheme也会展现出来!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats