本文主要是基于The Go Programming Language的读书摘要。之前写过几篇关于Go的文章和 一些实践goinaction

起源

go_stem

  • Go形成构想是在2007年9月,并于2009年11月发布。其发明人是Robert Griesemer, Rob Pike和Ken Thompson,这几位都任职于Google。
  • Go是个开源项目,所以其编译器,库和工具的源代码都可免费获取。
  • Go的运行环境包括,类UNIX系统 (Linux, FreeBSD, OpenBSD和Mac OS X),还有Plan 9Microsoft Windows。即,一份程序可以跨平台运行
  • Go是编译型的语言,其工具链将程序的源文件转变成机器相关的原生二进制指令,这些工具可以通过单一的go命令配合其子命令进行使用。
    • go run helloworld.go 将一个或多个.go为后缀的源文件进行编译,链接,并运行生成的可执行文件。
    • go build helloworld.go 编译,链接,生成二进制程序。
  • Go原生支持Unicode,所以它可以处理所有国家的语言。
  • Go平衡了表达力安全性。Go程序通常比动态语言程序运行更快,同时遭遇意料之外的类型错误而导致的崩溃更少。
  • Go语言从祖先语言中汲取的优点。(混血儿)
    • Go从C语言中继承了,表达式语法控制流语句基本数据类型按值调用的形参传递指针。但比这些更重要的是,继承了C所强调的程序要编译成高效的机器码,并自然地与所处的操作系统提供的抽象机制相配合。
    • Go从Pascal为发端的一个语言支流中
      • Modula-2启发了的概念。
      • Oberon消除了模块接口文件和模块实现文件的差异。
      • Oberon-2影响了包,导入和声明的语法,并提供了方法声明的语法。
    • Go借鉴了一些基于CSP (Communicating Sequential Process, 通信顺序进程)实现的语言
    • Go还有一些特性并非来自祖先的基因。例如,iota多多少少有点APL的影子;嵌套函数的词法作用域则来自Scheme。
    • Go还有自己一些全新的特性。例如,创新性的切片为动态数组提供了高效的随机访问的同时,也允许旧式链表的复杂共享机制;defer语句也是新引入的。
  • 简单性才是好软件的不二法门,Go保持极端简单性的行为文化
    • 在高级语言中,Go出现的较晚,因而有一定后发优势。它的基础部分实现不错。
      • 垃圾收集
      • 包系统
      • 一等函数
      • 词法作用域
      • 系统调用接口
      • 默认用UTF-8编码的字符串
    • 但相对来说,语言特性不多,而且不会增加新特性了。(Go成熟稳定,并且保证兼容更早版本)
      • 没有隐式数值类型强制转换
      • 没有构造或析构函数
      • 没有运算符重载
      • 没有形参默认值
      • 没有继承
      • 没有泛型
      • 没有异常
      • 没有
      • 没有函数注记
      • 没有线程局部存储
    • Go的类型系统,足可以使程序员避免在动态语言中会无意犯下的绝大多数错误,但相对而言,它在带类型的语言中,又算是类型系统比较简单的。Go能为程序员提供,具备相当强类型的系统才能实现的安全性和运行时性能,而不让程序员承担这种系统的复杂性。
    • Go提倡充分利用当代计算机系统设计,尤其强调局部性的重要
      • 由于现代计算机都是并行工作的,Go有着基于CSP的并行特性。并提供了变长栈来运行其轻量级线程(goroutine),这个栈初始化时非常小,所以创建一个goroutine成本极低,创建100万个也完全可以接受。
    • Go标准库常常被称为语言自带电池,它提供了清晰的构件,以及用于I/O文本处理图像加解密网络分布式应用的API,而且对许多标准文件格式和协议都提供了支持。

测试环境

根据The Go Programming Language Example Programs,构建测试环境。

安装

三种安装方式:

  • 源码安装。
  • 标准包安装。
  • 第三方工具安装。例如,Ubuntu的apt-get,Mac的homebrew
$go version
go version go1.17.6 linux/amd64

环境变量

编辑 ~/.bashrc 添加如下内容,之后执行 source ~/.bashrc

# go的安装路径
export GOROOT=/usr/local/go
export PATH=$GOROOT/bin:$PATH

# go的目标文件安装目录
export GOBIN=$HOME/go

# @refer https://learnku.com/go/t/39086#0b3da8
export GO111MODULE=on

refer: Compile and install the application

GOPATH

  • GOPATH允许多个目录(Linux下用冒号分割),当GOPATH指定了多个目录时,默认将go get的内容放在第一个目录
  • GOPATH目录约定有3个子目录:
    • src (源代码,例如,.go, .c, .h, .s等)
    • pkg (编译后生成的文件,例如,.a)
    • bin (编译后生成的可执行文件)

从 GOPATH 到 GO111MODULE

关于 GOPATH

当 Go 在 2009 年首次推出时,它并没有随包管理器一起提供。取而代之的是 go get,通过使用它们的导入路径来获取所有源并将其存储在 $GOPATH/src 中,没有版本控制并且 master 分支表示该软件包的稳定版本。

关于 Go Modules

Go 1.11 引入了 Go 模块。 Go Modules 不使用 GOPATH 存储每个软件包的单个 git checkout,而是存储带有 go.mod 的标记版本,并跟踪每个软件包的版本。

关于 GO111MODULE 环境变量

GO111MODULE 是一个环境变量,更改 Go 导入包的方式时进行设置。根据 Go 版本,其语义会发生变化

由于 GO111MODULE=on 允许你选择一个行为。如果不使用 Go Modules, go get 将会从模块代码的 master 分支拉取,而若使用 Go Modules 则你可以利用 Git Tag 手动选择一个特定版本的模块代码。

编译和执行

通过go get命令获取源码,构建和安装。

$go get gopl.io/ch1/helloworld

注意:从 Go 1.17 版本开始,go get 已废弃,不建议再使用。

Starting in Go 1.17, installing executables with go get is deprecated. go install may be used instead. In Go 1.18, go get will no longer build packages; it will only be used to add, update, or remove dependencies in go.mod. Specifically, go get will always act as if the -d flag were enabled.

go help get The -d flag instructs get not to build or install packages. get will only update go.mod and download source code needed to build packages.

go get: installing executables with 'go get' in module mode is deprecated.
        Use 'go install pkg@version' instead.
        For more information, see https://golang.org/doc/go-get-install-deprecation
        or run 'go help get' or 'go help install'.

源码在$(GOBIN)/src/gopl.io/ch1/helloworld/main.go

// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/

// See page 1.

// Helloworld is our first Go program.
//!+
package main

import "fmt"

func main() {
        fmt.Println("Hello, 世界")
}

//!-

编译执行:

$go build .
$ls
helloworld main.go
$./helloworld
Hello, 世界

Go入门示例

  • Go代码是使用来组织的,包类似其他语言中的库和模块。一个包由一个或多个.go源文件组成,放在一个文件夹中,该文件夹的名字描述了包的作用。
  • 每一个源文件的开始都用package声明。例如,package main,指明了这个文件属于哪个包。后面跟着它导入的其他包的列表,然后是存储在文件中的程序声明
  • Go的标准库中有100多个包用来完成输入,输出,排序,文本处理等常见任务。
  • 名为main的包用来定义一个独立的可执行程序,而不是库。
  • 必须精确地导入需要的包,在缺失导入,或存在不需要的包的情况下,编译会失败。(这种严格的要求可以防止程序演化中引用不需要的包)
    • goimports工具可以按需管理导入声明的插入和移除。go get golang.org/x/tools/cmd/goimports
    • 包的匿名导入,import ( _ "fmt") 对于没有显式用到的 package 通过匿名导入的方式忽略检查。
  • import声明必须跟在package声明之后。import导入声明后面,是组成程序的函数,变量,常量,类型声明。
  • Go不需要在语句或声明后使用分号结尾,除非有多个语句或声明出现在同一行。事实上,跟在特定符号后面的换行符被转换为分号。在什么地方进行换行会影响对Go代码的解析。例如,{符号必须和关键字func在同一行,不能独自成行。
  • Go对代码的格式化要求非常严格。gofmt工具将代码以标准格式重写。go工具的fmt子命令会用gofmt工具来格式化指定包里的所有文件,或者当前文件夹中的文件(默认情况下)。许多文本编辑器可以配置为在每次保存文件时自动运行gofmt,因此源文件总可以保持正确的形式。

命令行参数

  • os包提供了一些函数和变量,以平台无关的方式和操作系统打交道。命令行参数以os包中的Args名字的变量供程序访问。变量os.Args是一个字符串slice
// go get gopl.io/ch1/echo1
package main

import (
	"fmt"
	"os"
)

func main() {
	var s, sep string
	for i := 1; i < len(os.Args); i++ {
		s += sep + os.Args[i]
		sep = " "
	}
	fmt.Println(s)
}
  • 注释以\\开头。习惯上,在一个包声明前,使用注释对其进行描述。
  • var关键字声明了两个string类型的变量。变量可以在声明的时候初始化,如果变量没有明确地初始化,它将隐式地初始化为这个类型的空值。例如,对于数字初始化结果是0,对于字符串是空字符串。
  • 对于数字,Go提供常规的算术和逻辑操作符;对于字符串,+操作符表示追加操作。
  • :=符号用于短变量声明,这种语句声明一个或多个变量,并且根据初始化的值给予合适的类型。
  • 递增语句i++i进行加1,它等价于i += 1,又等价于i = i + 1注意,这些是语句,而不像其他C族语言一样是表达式,所以j = i ++是不合法的。并且,仅支持后缀,++i也不合法。
  • for是Go里面的唯一循环语句,它有几种形式。
    • for循环的三个组成部分两边不用小括号,但大括号是必需的,左大括号必须和post语句在同一行。
    • 可选的initialization语句在循环开始之前执行。如果存在,它必须是一个简单的语句。
    • condition是一个布尔表达式,在循环的每一次迭代开始前推演。
    • 三部分都可以省略。
for initialization; condition; post {
	// ...
}

// while循环
for condition {
	// ...
}

// 无限循环
for {
	// ...
	// break
	// return
}
  • 另一种形式的for循环字符串slice数据上迭代。
// Echo2 prints its command-line arguments.
package main

import (
	"fmt"
	"os"
)

func main() {
	s, sep := "", ""
	for _, arg := range os.Args[1:] {
		s += sep + arg
		sep = " "
	}
	fmt.Println(s)
}
  • 每一次迭代,range产生一对值:索引这个索引处元素的值
  • 在这个例子中,不需要索引,但是语法上range循环需要处理,因此也必须处理索引。一个主意是将索引赋予一个临时变量然后忽略它。但是,Go不允许存在无用的临时变量,不然会出现编译错误。解决方案是使用空标识符_空标识符可以用在任何语法需要变量名但是程序逻辑不需要的地方。例如,丢弃每次迭代产生的无用的索引。
  • 这个版本使用短的变量声明来声明和初始化。原则:使用显式的初始化来说明初始化变量的重要性,使用隐式的初始化来表明初始化变量不重要。
// 以下几种声明字符串变量的方式是等价的
s := ""              // 此方式,更加简洁,通常在一个函数内部使用,不适合包级别的变量 (推荐)
var s string         // 默认初始化为空字符串 (推荐)
var s = ""           // 很少用
var s string = ""    // 显式的变量类型,在类型一致的情况下是冗余的信息,在类型不一致时是必需的
  • 上面程序的问题:每次循环,字符串s有了新的内容,+=语句通过追加旧的字符串,空格字符,和下一个参数,生成一个新的字符串,然后把新字符串赋给s。旧的内容不再需要使用,会被例行垃圾回收。如果有大量的数据需要处理,这样的代价会比较大。(TODO: 测试确认)

  • 一个高效的方式是使用strings包中的Join函数。

// Echo3 prints its command-line arguments.
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	fmt.Println(strings.Join(os.Args[1:], " "))
}
  • 如果关心格式只是想看下值或调试,那么使用Println格式化结果就可以了。
// 任何slice都能以这样的方式输出
fmt.Println(os.Args[1:])

找出重复行

  • 用于文件复制,打印,检索,排序,统计的程序,通常有一个相似的结构:在输入接口上循环读取,然后对每一个元素进行一些计算。
// Dup1 prints the text of each line that appears more than
// once in the standard input, preceded by its count.
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	counts := make(map[string]int)
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		counts[input.Text()]++
	}
	// NOTE: ignoring potential errors from input.Err()
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}
  • for一样,if语句中的条件部分也不放在圆括号里,但是程序体中需要用到大括号。
  • map存储一个键/值对集合,并且提供常量时间的操作来存储,获取,或测试集合中某个元素。内置的函数make可以用来新建map。
    • 可以是其值能够进行相等比较的任意类型。比如,字符串。
    • 可以是任意类型。
  • 每次从输入读取一行内容,这一行就作为map中的键,对应的值递增1。
    counts[input.Text()]++
    // 等价于
    line := input.Text()
    counts[line] = counts[line] + 1
    
  • 在map中不存在时也是没有问题的。当一个新的行第一次出现时,右边的表达式counts[line]根据值类型被推演为零值,int的零值是0。
  • 为了输出结果,使用基于range的for循环,这次在map类型的counts变量上遍历。每次迭代输出两个结果,map里面一个元素对应的键和值。
  • map里面的键的迭代顺序不是固定的,通常是随机地。每次运行都不一致,这是有意设计的,以防止程序依赖某种特定的序列,此处不对排序做任何保证。
  • bufio包,可以简便和高效地处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以或者单词为单位断开,这是处理以行为单位的输入内容的最简单方式。扫描器从程序的标准输入进行读取。每一次调用input.Scan()读取下一行,并且将结尾的换行符去掉。通过input.Text()来获取读到的内容。Scan()函数在读到新行时候返回true,在没有更多内容的时候返回false
  • 函数fmt.Printf从一个表达式列表生成格式化的输出Printf函数有超过10个这样的转义字符,Go称为verb。例如:
verb 描述
%d 十进制整数
%x 十六进制
%o 八进制
%b 二进制
%f, %g, %e 浮点数: 如,3.141593, 3.141592653589793, 3.141593e+00
%t 布尔型: true或false
%c 字符(Unicode码点)
%s 字符串
%q 带引号字符串或字符
%v 内置格式的任何值
%T 任何值的类型
%% 百分号本身
  • 除了从标准输入读取,也可以从具体的文件读取。使用os.Open函数来逐个打开。
// Dup2 prints the count and text of lines that appear more than once
// in the input.  It reads from stdin or from a list of named files.
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	counts := make(map[string]int)
	files := os.Args[1:]
	if len(files) == 0 {
		countLines(os.Stdin, counts)
	} else {
		for _, arg := range files {
			f, err := os.Open(arg)
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue
			}
			countLines(f, counts)
			f.Close()
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}

func countLines(f *os.File, counts map[string]int) {
	input := bufio.NewScanner(f)
	for input.Scan() {
		counts[input.Text()]++
	}
	// NOTE: ignoring potential errors from input.Err()
}
  • 函数os.Open返回两个值,第一个是打开的文件*os.File;第二个是一个内置的error类型的值。
    • 如果error等于特殊的内置nil值,表示文件成功打开。文件在被读到结尾的时候,Close函数关闭文件,然后释放相应的资源。
    • 如果error不是nil,表示出错了。这时error的值描述错误原因。简单的错误处理是使用Fprintf%v在标准错误流上输出一条消息,%v可以使用默认格式显示任意类型的值。错误处理后,开始处理下一个文件。continue语句让循环进入下一个迭代。
  • map是一个使用make创建的数据结构的引用。当一个map传递给一个函数时,函数接收到这个引用的副本,所以,被调用函数中对于map数据结构中的改变,对函数调用者使用的map引用也是可见的
  • 第三种方式,是一次读取整个输入到大块内存中,一次性地分割所有行,然后处理这些行。使用ReadFile函数读取整个命名文件的内容,返回一个可以转化成字符串的字节slice,再用string.Split函数将一个字符串分割为一个由子串组成的slice
// Dup3 prints the count and text of lines that
// appear more than once in the named input files.
package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"strings"
)

func main() {
	counts := make(map[string]int)
	for _, filename := range os.Args[1:] {
		data, err := ioutil.ReadFile(filename)
		if err != nil {
			fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
			continue
		}
		for _, line := range strings.Split(string(data), "\n") {
			counts[line]++
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}
  • 实际上,bufio.Scannerioutil.ReadFile以及ioutil.WriteFile使用*os.File中的ReadWrite方法。但是大多数情况很少需要直接访问底层的函数,而是像bufioio/ioutil包中的上层的方法更易使用。

GIF动画

  • Go标准的图像包的使用,来创建一系列的位图图像,然后将位图序列编码为GIF动画。
  • 在导入由多段路径,如image/color组成的包之后,使用路径最后的一段来引用这个包。所以,color.White属于image/color包。
  • const声明用来给常量命名。const声明可以出现在包级别,或在一个函数内。常量必须是数字,字符串或布尔值。
  • 表达式[]color.Color{...}复合字面量,即用一系列元素的值初始化Go的复合类型的紧凑表达方式。这里,第一个是slice,第二个是结构体
  • gif.GIF是一个结构体类型。结构体由一组称为字段的值组成,字段通常有不同的数据类型,它们一起组成单个对象,作为一个单位被对待。anim变量是gif.GIF结构体类型,这个结构体字面量创建一个结构体LoopCount,其值设置为nframes,其他字段的值是对应类型的零值。结构体的每个字段可以通过点记法来访问。
// Lissajous generates GIF animations of random Lissajous figures.
package main

import (
	"image"
	"image/color"
	"image/gif"
	"io"
	"math"
	"math/rand"
	"os"
)

//!-main
// Packages not needed by version in book.
import (
	"log"
	"net/http"
	"time"
)

//!+main

var palette = []color.Color{color.White, color.Black}

const (
	whiteIndex = 0 // first color in palette
	blackIndex = 1 // next color in palette
)

func main() {
	//!-main
	// The sequence of images is deterministic unless we seed
	// the pseudo-random number generator using the current time.
	// Thanks to Randall McPherson for pointing out the omission.
	rand.Seed(time.Now().UTC().UnixNano())

	if len(os.Args) > 1 && os.Args[1] == "web" {
		//!+http
		handler := func(w http.ResponseWriter, r *http.Request) {
			lissajous(w)
		}
		http.HandleFunc("/", handler)
		//!-http
		log.Fatal(http.ListenAndServe("localhost:8000", nil))
		return
	}
	//!+main
	lissajous(os.Stdout)
}

func lissajous(out io.Writer) {
	const (
		cycles  = 5     // number of complete x oscillator revolutions
		res     = 0.001 // angular resolution
		size    = 100   // image canvas covers [-size..+size]
		nframes = 64    // number of animation frames
		delay   = 8     // delay between frames in 10ms units
	)
	freq := rand.Float64() * 3.0 // relative frequency of y oscillator
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0 // phase difference
	for i := 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
		img := image.NewPaletted(rect, palette)
		for t := 0.0; t < cycles*2*math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t*freq + phase)
			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
				blackIndex)
		}
		phase += 0.1
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim) // NOTE: ignoring encoding errors
}

获取一个URL

  • 从互联网获取信息。它获取每个指定URL的内容,然后不加解析的输出。类似curl这个工具。
  • 使用两个包:net/httpio/ioutil
  • http.Get产生一个HTTP请求。如果没有错,返回结果存在响应结构resp里面。其中,resp.Body包含服务器端响应的一个可读取数据流,随后通过ioutil.ReadAll读取整个响应结果并存入b
// Fetch prints the content found at each specified URL.
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
)

func main() {
	for _, url := range os.Args[1:] {
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
		b, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
			os.Exit(1)
		}
		fmt.Printf("%s", b)
	}
}

测试输出:

$./fetch http://gerryyang.com
<!DOCTYPE html>
<html lang="en"><head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1"><!-- Begin Jekyll SEO tag v2.7.1 -->
<title>Gerry’s blog | 他山之石,可以攻玉</title>

...

获取多个URL

  • Go最大的特点是支持并发编程,本例并发获取多个URL内容,于是这个进程使用的时间不超过耗时最长时间的获取任务,而不是所有获取任务总的时间。这个版本的实现丢弃响应的内容,但是报告每一个响应的大小和花费的时间。
  • goroutine是一个并发执行的函数。
  • 通道是一种允许某一例程向另一个例程传递指定类型的值的通信机制
  • main函数在一个goroutine中执行,然后go语句创建额外的goroutine
  • main函数使用make创建一个字符串通道,对于每一个命令行参数,go语句在第一轮循环中启动一个新的goroutine,它异步调用fetch来使用http.Get获取URL内容。
  • io.Copy函数读取响应的内容,然后通过写入ioutil.Discard输出流进行丢弃。Copy返回字节数以及出现的任何错误。每一个结果返回时,fetch发送一行汇总信息到通道ch
  • main中的第二轮循环接收并输出那些汇总行。

注意:当一个goroutine试图在一个通道上进行发送或接收操作时,它会阻塞,直到另一个goroutine试图进行接收或发送操作,才传递值,并开始处理两个goroutine。在示例中,每一个fetch在通道ch上发送一个值(ch <- expression),main函数接收它们(<-ch),由main来处理所有的输出确保来每个goroutine作为一个整体单元处理,这样就避免了两个goroutine同时完成造成输出交织带来的风险。

// Fetchall fetches URLs in parallel and reports their times and sizes.
package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

func main() {
	start := time.Now()
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		go fetch(url, ch) // start a goroutine
	}
	for range os.Args[1:] {
		fmt.Println(<-ch) // receive from channel ch
	}
	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
	start := time.Now()
	resp, err := http.Get(url)
	if err != nil {
		ch <- fmt.Sprint(err) // send to channel ch
		return
	}

	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
	resp.Body.Close() // don't leak resources
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs  %7d  %s", secs, nbytes, url)
}

测试输出:

$./fetchall http://gerryyang.com https://godoc.org http://baidu.com
1.13s       81  http://baidu.com
1.29s    33353  http://gerryyang.com
2.40s    17406  https://godoc.org
2.40s elapsed

一个Web服务器

  • 使用Go的库非常容易实现一个Web服务器,本例实现一个简单的Web服务,返回访问服务器的URL的路径部分。例如,如果请求的URL是http://localhost:8000/hello,则响应是URL.Path = "/hello"
  • main函数将一个处理函数和以/开头的URL链接在一起,代表所有的URL使用这个函数处理,然后启动服务器监听进入8000端口处的请求。
  • 一个请求由一个http.Request类型的结构体表示,它包含很多关联的域,其中一个是所请求的URL。
  • 当一个请求到达时,它被转交给处理函数,并从请求的URL中提取路径部分(/hello),使用fmt.Printf格式化,然后作为响应发送回去。
// Server1 is a minimal "echo" server.
package main

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

func main() {
	http.HandleFunc("/", handler) // each request calls handler
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

对上述功能进行扩展:

  • 这个服务器有两个处理函数,通过请求的URL来决定哪一个被调用。请求/count调用counter,其他的调用handler。以\结尾的处理模式匹配所有含有这个前缀的URL。
  • 对于传入的请求,服务器在不同的goroutine中运行该处理函数,这样它可以同时处理多个请求。然而,如果两个并发的请求试图同时更新计数值count,它可能会不一致地增加,程序会产生一个严重的竞态bug。为避免该问题,必现确保最多只有一个goroutine在同一时间访问变量,这正是mu.Lock()mu.Unlock()语句的作用。
// Server2 is a minimal "echo" and counter server.
package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

var mu sync.Mutex
var count int

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/count", counter)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	count++
	mu.Unlock()
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

// counter echoes the number of calls so far.
func counter(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	fmt.Fprintf(w, "Count %d\n", count)
	mu.Unlock()
}

作为更完整的例子,处理函数可以报告它接收到的消息头和表单数据,这样可以方便服务器审查和调试请求:

  • 注意,Go允许一个简单的语句(如一个局部变量声明)跟在if条件的前面,这在错误处理的时候特别有用,合并的语句更短而且可以缩小err变量的作用域,这是一个好实践。
// Server3 is an "echo" server that displays request parameters.
package main

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

func main() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

//!+handler
// handler echoes the HTTP request.
func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
	for k, v := range r.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
	fmt.Fprintf(w, "Host = %q\n", r.Host)
	fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
	if err := r.ParseForm(); err != nil {
		log.Print(err)
	}
	for k, v := range r.Form {
		fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
	}
}

其他内容

  • 控制流。除了两个基础的控制语句iffor,还有一个支持多路分支的switch语句。
  • 命名类型。type声明给已有类型命名。因为结构体类型通常很长,所以她们基本上都独立命名。
type Point struct {
	X, Y int
}
var p Point
  • 指针。Go提供了指针,它的值是变量的地址。

区别:在C语言中,指针基本上是没有约束的。Go做了一个折中,指针显式可见。使用&操作符可以获取一个变量的地址,使用*操作符可以获取指针引用的变量值。但是指针不支持算术运算。

  • 方法和接口。
    • 一个关联了命名类型的函数称为方法。Go里面的方法可以关联到几乎所有的命名类型。
    • 接口可以用相同的方式处理不同的具体类型的抽象类型,它基于这些类型所包含的方法,而不是类型的描述或实现。
  • 包。Go自带一个可扩展并且实用的标准库,Go社区创建和共享了更多的库。编程时,更多使用现有的包,而不是自己写所有的源码。
    • 标准库包的索引:https://golang.org/pkg
    • 社区贡献的包:https://godoc.org
    • 使用go doc工具可以方便地通过命令行访问这些文档
$go doc http.ListenAndServe
go: finding golang.org/x/sys v0.0.0-20210423082822-04245dca01da
go: finding golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
go: finding golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e
package http // import "net/http"

func ListenAndServe(addr string, handler Handler) error
    ListenAndServe listens on the TCP network address addr and then calls Serve
    with handler to handle requests on incoming connections. Accepted
    connections are configured to enable TCP keep-alives.

    The handler is typically nil, in which case the DefaultServeMux is used.

    ListenAndServe always returns a non-nil error.
  • 注释。在声明任何函数前,写一段注释来说明它的行为是一个好的风格,这个约定很重要,因为它们可以被go docgodoc工具定位和作为文档显示。对于跨越多行的注视,可以使用类似其他语言中的/* .... */注释。这样可以避免在文件的开始有一大块说明文本时每一行都有//。在注释内部,///*没有特殊的含义,所以注释不能嵌套。

Go程序组成

声明,变量,新类型,包和文件,以及作用域。

数值,布尔量,字符串,常量,Unicode

组合类型 (数组,键值对,结构体,切片)

函数,错误处理,崩溃和恢复,以及defer语句

方法,接口,并发,包,测试,反射

面向对象设计 (Go没有类继承,没有类,较复杂的对象行为是通过较简单的对象组合,而非继承完成的)

并发处理,基于CSP思想,采用goroutine和信道实现,共享变量

复合数据类型

JSON

  • 把 Go 的数据结构(比如 movies)转换为 JSON 称为marshal(通过json.Marshal来实现的),Marshal 生成了一个字节 slice,其中包含一个不带有任何多余空白字符的很长的字符串。为了方便阅读,可以使用json.MarshalIndent输出整齐格式化的结果,这个函数有两个参数,一个是定义每行输出的前缀字符串,另外一个是定义缩进的字符串。
  • marshal使用 Go 结构体成员的名称作为 JSON 对象里面字段的名称(通过反射的方式),只有可导出的成员(首字母大写)可以转换为 JSON 字段。
  • 结构体成员 Year 对应地转换为 releasedColor转换为color,这个是通过成员标签定义(field tag)实现的。成员标签定义是结构体成员在编译期间关联的一些元信息。标签值的第一部分指定了 Go 结构体成员对应 JSON 中字段的名字,Color标签还有一个额外的选项omitempty,它表示如果这个成员的值是零值或者为空,则不输出这个成员到 JSON 中。
  • 将 JSON 字符串解码为 Go 数据结构,这个过程叫做unmarshal,这个是由json.Unmarshal实现的。通过合理地定义 Go 的数据结构,可以选择将哪部分 JSON 数据解码到结构对象中,哪些数据可以丢弃。例如,当函数Unmarshal调用完成后,它将填充结构体sliceTitle的值,JSON 中其他的字段就丢弃了。
// Movie prints Movies as JSON.
package main

import (
	"encoding/json"
	"fmt"
	"log"
)

//!+
type Movie struct {
	Title  string
	Year   int  `json:"released"`
	Color  bool `json:"color,omitempty"`
	Actors []string
}

var movies = []Movie{
	{Title: "Casablanca", Year: 1942, Color: false,
		Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
	{Title: "Cool Hand Luke", Year: 1967, Color: true,
		Actors: []string{"Paul Newman"}},
	{Title: "Bullitt", Year: 1968, Color: true,
		Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
	// ...
}

//!-

func main() {
	{
		//!+Marshal
		data, err := json.Marshal(movies)
		if err != nil {
			log.Fatalf("JSON marshaling failed: %s", err)
		}
		fmt.Printf("%s\n", data)
		//!-Marshal
	}

	{
		//!+MarshalIndent
		data, err := json.MarshalIndent(movies, "", "    ")
		if err != nil {
			log.Fatalf("JSON marshaling failed: %s", err)
		}
		fmt.Printf("%s\n", data)
		//!-MarshalIndent

		//!+Unmarshal
		var titles []struct{ Title string }
		if err := json.Unmarshal(data, &titles); err != nil {
			log.Fatalf("JSON unmarshaling failed: %s", err)
		}
		fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
		//!-Unmarshal
	}
}

输出:

$./movie
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingrid Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Actors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"Actors":["Steve McQueen","Jacqueline Bisset"]}]
[
    {
        "Title": "Casablanca",
        "released": 1942,
        "Actors": [
            "Humphrey Bogart",
            "Ingrid Bergman"
        ]
    },
    {
        "Title": "Cool Hand Luke",
        "released": 1967,
        "color": true,
        "Actors": [
            "Paul Newman"
        ]
    },
    {
        "Title": "Bullitt",
        "released": 1968,
        "color": true,
        "Actors": [
            "Steve McQueen",
            "Jacqueline Bisset"
        ]
    }
]
[{Casablanca} {Cool Hand Luke} {Bullitt}]

Github

  • 由于用户的查询请求参数中可能存在一些字符,这些字符在 URL 中是特殊字符,比如?或者&,因此使用url.QueryEscape函数来确保它们拥有正确的含义。
  • 这里使用流式解码器json.Decoder来依次从字节流里面解码出多个JSON实体。对应有一个json.Encoder的流式编码器。
// github.go

// Package github provides a Go API for the GitHub issue tracker.
// See https://developer.github.com/v3/search/#search-issues.
package github

import "time"

const IssuesURL = "https://api.github.com/search/issues"

type IssuesSearchResult struct {
	TotalCount int `json:"total_count"`
	Items      []*Issue
}

type Issue struct {
	Number    int
	HTMLURL   string `json:"html_url"`
	Title     string
	State     string
	User      *User
	CreatedAt time.Time `json:"created_at"`
	Body      string    // in Markdown format
}

type User struct {
	Login   string
	HTMLURL string `json:"html_url"`
}
// search.go

package github

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

// SearchIssues queries the GitHub issue tracker.
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
	q := url.QueryEscape(strings.Join(terms, " "))
	resp, err := http.Get(IssuesURL + "?q=" + q)
	if err != nil {
		return nil, err
	}
	//!-
	// For long-term stability, instead of http.Get, use the
	// variant below which adds an HTTP request header indicating
	// that only version 3 of the GitHub API is acceptable.
	//
	//   req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil)
	//   if err != nil {
	//       return nil, err
	//   }
	//   req.Header.Set(
	//       "Accept", "application/vnd.github.v3.text-match+json")
	//   resp, err := http.DefaultClient.Do(req)
	//!+

	// We must close resp.Body on all execution paths.
	// (Chapter 5 presents 'defer', which makes this simpler.)
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("search query failed: %s", resp.Status)
	}

	var result IssuesSearchResult
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		resp.Body.Close()
		return nil, err
	}
	resp.Body.Close()
	return &result, nil
}

主程序:

// main.go

// Issues prints a table of GitHub issues matching the search terms.
package main

import (
	"fmt"
	"log"
	"os"

	"gopl.io/ch4/github"
)

//!+
func main() {
	result, err := github.SearchIssues(os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d issues:\n", result.TotalCount)
	for _, item := range result.Items {
		fmt.Printf("#%-5d %9.9s %.55s\n",
			item.Number, item.User.Login, item.Title)
	}
}

/*
$ go build gopl.io/ch4/issues
$ ./issues repo:golang/go is:open json decoder
13 issues:
#5680    eaigner encoding/json: set key converter on en/decoder
#6050  gopherbot encoding/json: provide tokenizer
#8658  gopherbot encoding/json: use bufio
#8462  kortschak encoding/json: UnmarshalText confuses json.Unmarshal
#5901        rsc encoding/json: allow override type marshaling
#9812  klauspost encoding/json: string tag not symmetric
#7872  extempora encoding/json: Encoder internally buffers full output
#9650    cespare encoding/json: Decoding gives errPhase when unmarshalin
#6716  gopherbot encoding/json: include field name in unmarshal error me
#6901  lukescott encoding/json, encoding/xml: option to treat unknown fi
#6384    joeshaw encoding/json: encode precise floating point integers u
#6647    btracey x/tools/cmd/godoc: display type kind of each named type
#4237  gjemiller encoding/base64: URLEncoding padding is optional
*/

HTML模版

  • 当要求格式和代码彻底分离,这个额可以通过text/templatehtml/template里面的方法实现。这两个包提供了一种机制,可以将程序变量的值代入到文本或者HTML模版中。
  • 模版,是一个字符串或文件,它包含一个或者多个,两边用双大括号包围的单元{{...}},这个称为操作。每个操作在模版语言里都对应一个表达式,提供的功能包括:输出值,选择结构体成员,调用函数和方法,描述控制逻辑,实例化其他的模版等。
  • 通过模版输出结果需要两个步骤
    • 需要解析模版并转换为内部的表示方法(解析模版只需要执行一次)
    • 然后在指定的输入上面执行
  • 创建并解析定义的文本模版templ,注意方法的链式调用:template.New创建并返回一个新的模版,Funcs添加daysAgo到模版内部可以访问的函数列表中,然后返回这个模版对象;最后调用Parse方法。
  • 更多:go doc text/template go doc html/template
// Issuesreport prints a report of issues matching the search terms.
package main

import (
	"log"
	"os"
	"text/template"
	"time"

	"gopl.io/ch4/github"
)

//!+template
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`

//!-template

//!+daysAgo
func daysAgo(t time.Time) int {
	return int(time.Since(t).Hours() / 24)
}

//!-daysAgo

//!+exec
var report = template.Must(template.New("issuelist").
	Funcs(template.FuncMap{"daysAgo": daysAgo}).
	Parse(templ))

func main() {
	result, err := github.SearchIssues(os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	if err := report.Execute(os.Stdout, result); err != nil {
		log.Fatal(err)
	}
}

//!-exec

func noMust() {
	//!+parse
	report, err := template.New("report").
		Funcs(template.FuncMap{"daysAgo": daysAgo}).
		Parse(templ)
	if err != nil {
		log.Fatal(err)
	}
	//!-parse
	result, err := github.SearchIssues(os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	if err := report.Execute(os.Stdout, result); err != nil {
		log.Fatal(err)
	}
}
/*
//!+output
$ go build gopl.io/ch4/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
13 issues:
----------------------------------------
Number: 5680
User:   eaigner
Title:  encoding/json: set key converter on en/decoder
Age:    750 days
----------------------------------------
Number: 6050
User:   gopherbot
Title:  encoding/json: provide tokenizer
Age:    695 days
----------------------------------------
...
//!-output
*/

html/template包和text/template包里面使用一样的API和表达式语句。

// Issueshtml prints an HTML table of issues matching the search terms.
package main

import (
	"log"
	"os"

	"gopl.io/ch4/github"
)

//!+template
import "html/template"

var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
  <th>#</th>
  <th>State</th>
  <th>User</th>
  <th>Title</th>
</tr>
{{range .Items}}
<tr>
  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
  <td>{{.State}}</td>
  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
  <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))

//!-template

//!+
func main() {
	result, err := github.SearchIssues(os.Args[1:])
	if err != nil {
		log.Fatal(err)
	}
	if err := issueList.Execute(os.Stdout, result); err != nil {
		log.Fatal(err)
	}
}

./issueshtml repo:golang/go commenter:gopherbot json encoder > issues.html

<h1>65 issues</h1>
<table>
<tr style='text-align: left'>
  <th>#</th>
  <th>State</th>
  <th>User</th>
  <th>Title</th>
</tr>

<tr>
  <td><a href='https://github.com/golang/go/issues/7872'>7872</a></td>
  <td>open</td>
  <td><a href='https://github.com/extemporalgenome'>extemporalgenome</a></td>
  <td><a href='https://github.com/golang/go/issues/7872'>encoding/json: Encoder internally buffers full output</a></td>
</tr>

<tr>
  <td><a href='https://github.com/golang/go/issues/5901'>5901</a></td>
  <td>open</td>
  <td><a href='https://github.com/rsc'>rsc</a></td>
  <td><a href='https://github.com/golang/go/issues/5901'>encoding/json: allow per-Encoder/per-Decoder registration of marshal/unmarshal functions</a></td>
</tr>

issueshtml

注意:html/template包自动将 HTML 元字符转义,这样显示才能正常。

// Autoescape demonstrates automatic HTML escaping in html/template.
package main

import (
	"html/template"
	"log"
	"os"
)

//!+
func main() {
	const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
	t := template.Must(template.New("escape").Parse(templ))
	var data struct {
		A string        // untrusted plain text
		B template.HTML // trusted HTML
	}
	data.A = "<b>Hello!</b>"
	data.B = "<b>Hello!</b>"
	if err := t.Execute(os.Stdout, data); err != nil {
		log.Fatal(err)
	}
}

输出:

$./autoescape
<p>A: &lt;b&gt;Hello!&lt;/b&gt;</p><p>B: <b>Hello!</b></p>

惯例用法

Three Dots

3 dots in 4 places

Variadic function parameters

You can pass a slice s directly to a variadic function if you unpack it with the s... notation. In this case no new slice is created. In this example, we pass a slice to the Sum function.

primes := []int{2, 3, 5, 7}
fmt.Println(Sum(primes...)) // 17

Array literals

In an array literal, the ... notation specifies a length equal to the number of elements in the literal.

stooges := [...]string{"Moe", "Larry", "Curly"} // len(stooges) == 3

The go command

Three dots are used by the go command as a wildcard when describing package lists. This command tests all packages in the current directory and its subdirectories.

$ go test ./...

模版生成代码(go generate / ast)

通过go generate命令解析go的源代码,并生成新的代码文件。

例子:

  • https://pkg.go.dev/golang.org/x/tools/cmd/stringer
  • https://github.com/mohuishou/gen-const-msg

refer:

  • https://lailin.xyz/post/41140.html?f=tt
  • https://pkg.go.dev/go/ast

Golang Runtime

TODO

各种函数的用法

https://blog.csdn.net/delphiwcdj/article/details/17611699

常用的几种函数用法,主要包括:

  • 首先main是一个没有返回值的函数
  • 普通函数
  • 函数返回多个值
  • 不定参函数
  • 闭包函数
  • 递归函数
  • 类型方法, 类似C++中类的成员函数
  • 接口和多态
  • Defer 接口
  • 错误处理, Panic/Recover
package main

import (
	"fmt"
	"math"
	"errors"
	"os"
	"io"
)


///
func max(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

func multi_ret(key string) (int, bool) {
	m := map[string]int{"one":1, "two":2, "three":3}
	var err bool
	var val int
	val, err = m[key]
	return val, err
}

func sum(nums ...int) {
	fmt.Print(nums, " ")
	total := 0
	for _, num := range nums {
		total += num
	}
	fmt.Println(total)
}

func nextNum() func() int {
	i, j := 0, 1
	return func() int {
		var tmp = i + j
		i, j = j, tmp
		return tmp
	}
}

func fact(n int) int {
	if n == 0 {
		return 1
	}
	return n * fact(n-1)
}

// 长方形
type rect struct {
	width, height float64
}

func (r *rect) area() float64 {
	return r.width * r.height
}

func (r *rect) perimeter() float64 {
	return 2 * (r.width + r.height)
}

// 圆形
type circle struct {
	radius float64
}

func (c *circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c *circle) perimeter() float64 {
	return 2 * math.Pi * c.radius
}

// 接口
type shape interface {
	area() float64
	perimeter() float64
}

func interface_test() {
	r := rect{width: 2, height: 4}
	c := circle{radius: 4.3}

	// 通过指针实现
	s := []shape{&r, &c}

	for _, sh := range s {
		fmt.Println(sh)
		fmt.Println(sh.area())
		fmt.Println(sh.perimeter())
	}
}

type myError struct {
	arg int
	errMsg string
}

// 实现error的Error()接口
func (e *myError) Error() string {
	return fmt.Sprintf("%d - %s", e.arg, e.errMsg)
}

func error_test(arg int) (int, error) {
	if arg < 0 {
		return -1, errors.New("Bad Arguments, negtive")
	} else if arg > 256 {
		return -1, &myError{arg, "Bad Arguments, too large"}
	}
	return arg * arg, nil
}

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		fmt.Println("Open failed")
		return
	}
	defer src.Close()

	dst, err := os.Create(dstName)
	if err != nil {
		fmt.Println("Create failed")
		return
	}
	defer dst.Close()

	// 注意dst在前面
	return io.Copy(dst, src)
}


//
func main() {

	// [0] 首先main是一个没有返回值的函数

	// [1] 普通函数
	fmt.Println(max(1, 100))

	// [2] 函数返回多个值
	v, e := multi_ret("one")
	fmt.Println(v, e)
	v, e = multi_ret("four")
	fmt.Println(v, e)
	// 典型的判断方法
	if v, e = multi_ret("five"); e {
		fmt.Println("OK")
	} else {
		fmt.Println("Error")
	}

	// [3] 不定参函数
	sum(1, 2)
	sum(2, 4, 5)
	nums := []int{1, 2, 3, 4, 5}
	sum(nums...)

	// [4] 闭包函数
	nextNumFunc := nextNum()
	for i := 0; i < 10; i++ {
		fmt.Println(nextNumFunc())
	}

	// [5] 递归函数
	fmt.Println(fact(4))

	// [6] 类型方法, 类似C++中类的成员函数
	r := rect{width: 10, height: 15}
	fmt.Println("area: ", r.area())
	fmt.Println("perimeter: ", r.perimeter())

	rp := &r
	fmt.Println("area: ", rp.area())
	fmt.Println("perimeter: ", rp.perimeter())

	// [7] 接口和多态
	interface_test()

	// [8] 错误处理, Error接口
	for _, val := range []int{-1, 4, 1000} {
		if r, e := error_test(val); e != nil {
			fmt.Printf("failed: %d:%s\n", r, e)
		} else {
			fmt.Println("success: ", r, e)
		}
	}

	// [9] 错误处理, Defer接口
	if w, err := CopyFile("/data/home/gerryyang/dst_data.tmp", "/data/home/gerryyang/src_data.tmp"); err != nil {
		fmt.Println("CopyFile failed: ", e)
	} else {
		fmt.Println("CopyFile success: ", w)
	}

	// 你猜下面会打印什么内容
	fmt.Println("beg ------------")
	for i := 0; i < 5; i++ {
		defer fmt.Printf("%d ", i)
	}
	fmt.Println("end ------------")

	// [10] 错误处理, Panic/Recover
	// 可参考相关资料, 此处省略

}
/*
output:
100
1 true
0 false
Error
[1 2] 3
[2 4 5] 11
[1 2 3 4 5] 15
1
2
3
5
8
13
21
34
55
89
24
area:  150
perimeter:  50
area:  150
perimeter:  50
&{2 4}
8
12
&{4.3}
58.088048164875275
27.01769682087222
failed: -1:Bad Arguments, negtive
success:  16 <nil>
failed: -1:1000 - Bad Arguments, too large
CopyFile success:  8
------------
------------
4 3 2 1 0
*/

Tips

go -ldflags 信息注入

构建时指定 -mod=vendor 的作用

在使用 Go 1.11 及以上版本的时候,Go 引入了 Go Modules 的特性,用于管理项目的依赖关系。在使用 Go Modules 的时候,可以通过在项目根目录下创建 go.mod 文件来指定项目的依赖关系。

在使用 Go Modules 的时候,可以通过 go build 命令来构建项目。如果项目依赖的包已经被下载到本地缓存中,go build 命令会自动使用本地缓存中的包。如果本地缓存中没有需要的包,go build 命令会从远程仓库中下载需要的包。

在使用 Go Modules 的时候,可以通过 -mod 参数来指定包的下载方式。其中,-mod=vendor 表示优先使用项目根目录下的 vendor 目录中的包,如果 vendor 目录中没有需要的包,则从远程仓库中下载需要的包。

使用 -mod=vendor 的好处是可以将项目依赖的包保存在项目根目录下的 vendor 目录中,避免了依赖包的版本冲突和不稳定性。同时,也可以避免在构建项目时从远程仓库中下载依赖包,提高了构建的速度和稳定性。

需要注意的是,使用 -mod=vendor 的时候,需要在项目根目录下创建 vendor 目录,并将依赖的包复制到 vendor 目录中。可以使用 go mod vendor 命令来自动将依赖的包复制到 vendor 目录中。

标准库

exec

Package exec runs external commands. It wraps os.StartProcess to make it easier to remap stdin and stdout, connect I/O with pipes, and do other adjustments.

exec 包是 Go 语言中用于运行外部命令的标准库。它封装了 os.StartProcess 函数,使得重定向标准输入输出、使用管道连接 I/O 等操作更加方便。

与其他语言中的 “system” 库调用不同,os/exec 包有意地不调用系统 shell,并且不会扩展任何通配符模式或处理其他扩展、管道或重定向,这通常是 shell 所做的。该包的行为更像 C 语言中的 “exec” 函数族。要扩展通配符模式,请直接调用 shell,注意转义任何危险的输入,或使用 path/filepath 包的 Glob 函数。要扩展环境变量,请使用 os 包的 ExpandEnv 函数。

https://pkg.go.dev/os/exec#Cmd.Start

package main

import (
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("sleep", "5")
	err := cmd.Start()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Waiting for command to finish...")
	err = cmd.Wait()
	log.Printf("Command finished with error: %v", err)
}
package main

import (
	"context"
	"fmt"
	"os/exec"
	"time"
)

func main() {
	// 创建一个上下文对象,设置超时时间为 5 秒
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 创建一个命令对象
	cmd := exec.CommandContext(ctx, "ls", "-l")

	// 执行命令,并获取输出
	output, err := cmd.Output()

	// 判断命令是否执行成功
	if err != nil {
		fmt.Println("Command failed:", err)
		return
	}

	// 打印命令的输出
	fmt.Println(string(output))
}

代码检查

golangci-lint

golangci-lint is a Go linters aggregator.

# binary will be $(go env GOPATH)/bin/golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.2

golangci-lint --version

静态代码分析 Staticcheck

Staticcheck is a state of the art linter for the Go programming language. Using static analysis, it finds bugs and performance issues, offers simplifications, and enforces style rules.

Each of the 150+ checks has been designed to be fast, precise and useful. When Staticcheck flags code, you can be sure that it isn’t wasting your time with unactionable warnings. Unlike many other linters, Staticcheck focuses on checks that produce few to no false positives. It’s the ideal candidate for running in CI without risking spurious failures.

Staticcheck aims to be trivial to adopt. It behaves just like the official go tool and requires no learning to get started with. Just run staticcheck ./... on your code in addition to go vet ./... .

While checks have been designed to be useful out of the box, they still provide configuration where necessary, to fine-tune to your needs, without overwhelming you with hundreds of options.

Staticcheck can be used from the command line, in CI, and even directly from your editor.

Ignoring problems with linter directives

  • https://stackoverflow.com/questions/70208440/how-to-disable-golang-unused-function-error
//lint:ignore U1000 Ignore unused function temporarily for debugging

数据竞争检查 (go build -race)

go build -race 命令是 Go 语言工具链中的一个选项,用于启用数据竞争检测器。数据竞争是指两个或多个并发执行的线程访问同一个内存位置,其中至少一个线程执行写操作,而这些线程的执行顺序是不确定的。数据竞争可能导致程序行为不稳定和不可预测。

-race 选项在编译和链接阶段启用数据竞争检测器,它会在运行时检测数据竞争。当你使用 -race 选项构建 Go 程序时,程序会在运行时检测潜在的数据竞争问题。如果检测到数据竞争,程序会报告竞争条件并退出,同时返回非零退出状态。

请注意,启用数据竞争检测器会增加程序的运行时开销。因此,通常在开发和测试阶段使用 -race 选项来识别和修复数据竞争问题,而在生产环境中,不建议使用 -race 选项。

测试代码:

package main

import (
    "fmt"
    "sync"
)

var counter int

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            counter++
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter)
}

go build -race main.go

测试输出:

./main
==================
WARNING: DATA RACE
Read at 0x000001216848 by goroutine 9:
  main.main.func1()
      /Users/gerry/Proj/github/goinaction/src/race_check/main.go:16 +0x32

Previous write at 0x000001216848 by goroutine 6:
  main.main.func1()
      /Users/gerry/Proj/github/goinaction/src/race_check/main.go:16 +0x4a

Goroutine 9 (running) created at:
  main.main()
      /Users/gerry/Proj/github/goinaction/src/race_check/main.go:15 +0x64

Goroutine 6 (running) created at:
  main.main()
      /Users/gerry/Proj/github/goinaction/src/race_check/main.go:15 +0x64
==================
Counter: 990
Found 1 data race(s)

问题调试 (delve)

生成 coredump 文件后,可以使用 delve 来调试 coredump 文件,delve 是一款专门为 Go 语言开发的调试器,它能够提供丰富的调试功能。源码地址 https://github.com/go-delve/delve/tree/master/Documentation/installation

服务启动前加上 GOTRACEBACK=crash,可以生成 corefile,和 gdb 类似,可以用 dlv 进行调试。

ulimit -c unlimited
export GOTRACEBACK=crash

安装:

go install github.com/go-delve/delve/cmd/dlv@latest

GOTRACEBACK 是一个环境变量,用于控制当 Go 程序崩溃时,运行时系统生成的调试信息的详细程度。这些调试信息通常包括堆栈跟踪(stack trace),帮助开发者定位问题。

GOTRACEBACK 的可选参数如下:

  • none:不产生任何调试信息。
  • single:只显示当前 goroutine的堆栈跟踪,如果没有设置 GOTRACEBACK 环境变量,将默认使用此选项。
  • all:显示所有 goroutine 的堆栈跟踪。
  • system:显示所有 goroutine 的堆栈跟踪,包括运行时系统的 goroutine。
  • crash:与 system 类似,但在生成堆栈跟踪后,程序会通过调用操作系统的 crash 功能来终止,这对于生成核心转储文件(core dump)以便进一步分析非常有用。

调试 coredump 文件执行指令:

dlv core your_program your_corefile --check-go-version=false

--check-go-version=false 是忽略 go 版本和 dlv 版本的区别,不然会报错。

  • 执行 goroutinesgrs 指令,查看执行的协程
  • 使用命令 goroutine $协程ID 对当前的 goroutine 进行切换,然后 bt 查看堆栈信息
  • 如果希望查看更多的堆栈帧,可以使用 bt -full ,它将显示完整的堆栈跟踪,包括函数参数和局部变量
  • 如果只想查看特定堆栈帧的详细信息,可以使用 frame 命令,后跟堆栈帧的编号
  • 使用 grsbt 命令时如果需要翻页,可以使用 les 进行翻页操作

dlv_debug

Tools

golang 百科全书

https://awesome-go.com/

golang developer roadmap

https://github.com/Alikhll/golang-developer-roadmap

sql2go 工具

http://stming.cn/tool/sql2go.html

toml2go 工具

https://xuri.me/toml-to-go/

curl2go 工具

https://mholt.github.io/curl-to-go/

json2go 工具

https://mholt.github.io/json-to-go/

泛型工具

https://github.com/cheekybits/genny

QR Code encoder (二维码生成工具)

  • 通过第三方服务生成。qrserver 提供的二维码生成服务,参考API 文档

  • 通过 https://github.com/skip2/go-qrcode 或 https://github.com/yeqown/go-qrcode 方案自己生成。

package main

import (
	"github.com/skip2/go-qrcode"
)

func main() {
	var url = "http://gerryyang.com"
	err := qrcode.WriteFile(url, qrcode.Medium, 256, "qr.png")
	if err != nil {
		panic(err)
	}
}

Protocol Buffers go_package

Go Conference

https://github.com/gopherchina/conference

Manual

开源代码

https://github.com/urfave/cli

cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.

https://github.com/gammazero/deque

Fast ring-buffer deque (double-ended queue) implementation. For a pictorial description, see the Deque diagram

Q&A

Refer