Go常用标准库

常用标准库

fmt

fmt

fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

输出

Print

Print系列函数会将内容输出到系统的标准输出,区别在于:

  • Print()函数直接输出内容
  • Printf()函数支持格式化输出字符串
  • Println()函数会在输出内容的结尾添加一个换行符
1
2
3
func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

举个简单的例子:

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
fmt.Print("在终端打印该信息。")
name := "qingbo1011.top"
fmt.Printf("我是:%s\n", name)
fmt.Println("在终端打印单独一行显示")
}

输出:

Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

1
2
3
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"os"
)

func main() {
// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
openFile, err := os.OpenFile("./test.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("打开文件出错,err:", err)
return
}
name := "qingbo1011.top"
// 向打开的文件句柄中写入内容
fmt.Fprintf(openFile, "往文件中写入信息:", name)
}

注意,只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串

1
2
3
func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
str1 := fmt.Sprint("qingbo1011.top")
name := "张三"
age := 20
str2 := fmt.Sprintf("name:%s, age:%v ", name, age)
str3 := fmt.Sprintln("qingbo")
fmt.Println(str1, str2, str3)
}

输出结果:

Errorf

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误

1
func Errorf(format string, a ...interface{}) error

通常使用这种方式来自定义错误类型,例如:

1
err := fmt.Errorf("这是一个错误")

格式化占位符

格式化占位符

这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

通用占位符

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号

布尔型

占位符 说明
%t true或false

整型

占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

字符串和[]byte

占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f
%X 每个字节用两字符十六进制数表示(使用A-F)

指针

占位符 说明
%p 表示为十六进制,并加上前导的0x

宽度标识符

宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0

其他falg

占位符 说明
+ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
- 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐)
# 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值
0 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
s := "张三"
fmt.Printf("%s\n", s)
fmt.Printf("%5s\n", s)
fmt.Printf("%-5s\n", s)
fmt.Printf("%5.7s\n", s)
fmt.Printf("%-5.7s\n", s)
fmt.Printf("%5.2s\n", s)
fmt.Printf("%05s\n", s)
}

输出结果:

1
2
3
4
5
6
7
张三
张三
张三
张三
张三
张三
000张三

输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

1
func Scan(a ...interface{}) (n int, err error)
  • Scan()从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"log"
)

func main() {
var (
name string
age int
married bool
)
fmt.Print("请分别输入name age和married:")
fmt.Scan(&name, &age, &married)
fmt.Println(name, age, married)
}

fmt.Scan从标准输入中扫描用户输入的数据,将以空白符分隔的数据分别存入指定的参数。

fmt.Scanf

函数签名如下:

1
func Scanf(format string, a ...interface{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
var (
name string
age int
married bool
)
fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

fmt.Scanf不同于fmt.Scan简单的以空格作为输入数据的分隔符,fmt.Scanf为输入数据指定了具体的输入内容格式,只有按照格式输入数据才会被扫描并存入对应变量。

fmt.Scanln

函数签名如下:

1
func Scanln(a ...interface{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

具体代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
var (
name string
age int
married bool
)
fmt.Scanln(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

fmt.Scanln遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

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

func main() {
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Print("请输入内容:")
text, _ := reader.ReadString('\n') // 读到换行
text = strings.TrimSpace(text)
fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

1
2
3
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

Sscan系列

这几个函数功能分别类似于fmt.Scanfmt.Scanffmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

1
2
3
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

Time

Time

时间和日期是编程中经常会用到的,这里学习一下Go语言内置的time包的基本用法。

time包提供了时间的显示和测量用的函数。日历的计算采用的是公历。

时间类型

time.Time类型表示时间。我们可以通过time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now() // 获取当前时间
fmt.Printf("current time:%v\n", now) // current time:2022-06-27 22:47:59.3370572 +0800 CST m=+0.007709001

year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second) // 2022-06-27 22:47:59
}

时间戳

时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。

基于时间对象获取时间戳的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now() // 获取当前时间
timestamp1 := now.Unix() // 时间戳
timestamp2 := now.UnixNano() // 纳秒时间戳
fmt.Printf("current timestamp1:%v\n", timestamp1) // current timestamp1:1656341407
fmt.Printf("current timestamp2:%v\n", timestamp2) // current timestamp2:1656341407763484600
}

使用time.Unix()函数可以将时间戳转为时间格式:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"time"
)

func main() {
timestamp := int64(1656341407)
t := time.Unix(timestamp, 0) // 将时间戳转为时间格式
fmt.Println(t) // 2022-06-27 22:50:07 +0800 CST
}

时间间隔

time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。

time包中定义的时间间隔类型的常量如下:

1
2
3
4
5
6
7
8
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)

例如:time.Duration表示1纳秒,time.Second表示1秒。

时间操作

Add

我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:

1
func (t Time) Add(d Duration) Time

举个例子,求一个小时之后的时间:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now()
later := now.Add(time.Hour) // 当前时间加1小时后的时间
fmt.Println(later) // 2022-06-27 23:54:46.2589154 +0800 CST m=+3600.006495401
}

Sub

求两个时间之间的差值:

1
func (t Time) Sub(u Time) Duration

返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。

Equal

1
func (t Time) Equal(u Time) bool

判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息

Before

1
func (t Time) Before(u Time) bool

如果t代表的时间点在u之前,返回true;否则返回false

After

1
func (t Time) After(u Time) bool

如果t代表的时间点在u之后,返回true;否则返回false

定时器

使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"time"
)

func main() {
ticker := time.Tick(time.Second * 2) // 定义一个2秒间隔的定时器
for i := range ticker {
fmt.Println(i) // 每2秒都会执行的任务
}
}

时间格式化

时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S,而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。也许这就是技术人员的浪漫吧。(补充:如果想格式化为12小时方式,需指定PM。)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"time"
)

func main() {
now := time.Now() // 获取当前时间
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
// 24小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan")) // 2022-06-27 22:59:51.212 Mon Jun
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan")) // 2022-06-27 10:59:51.212 PM Mon Jun
fmt.Println(now.Format("2006/01/02 15:04")) // 2022/06/27 22:59
fmt.Println(now.Format("15:04 2006/01/02")) // 22:59 2022/06/27
fmt.Println(now.Format("2006/01/02")) // 2022/06/27
}

解析字符串格式的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"log"
"time"
)

func main() {
now := time.Now()
fmt.Println(now) // 2022-06-27 23:05:34.8975132 +0800 CST m=+0.006696001
// 加载时区
location, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
log.Fatal(err)
}
// 按照指定时区和指定格式解析字符串时间
str := "2022/06/28 21:15:20" // 时间字符串
t, err := time.ParseInLocation("2006/01/02 15:04:05", str, location)
if err != nil {
log.Fatal(err)
}
fmt.Println(t) // 2022-06-28 21:15:20 +0800 CST
fmt.Println(t.Sub(now)) // 22h9m45.1024868s
}

string转time

最近在工作的时候,遇到过将腾讯云返回的时间和阿里云返回的时间字符串转成time.Time类型的问题。这里记录一下。

腾讯云中返回的时间字符串为:"2019-03-24T12:52:28Z"。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"log"
"time"
)

func main() {
str := "2019-03-24T12:52:28Z"
t, err := time.Parse("2006-01-02T15:04:05Z", str)
if err != nil {
log.Fatalln(err)
}
fmt.Println(t)
}

阿里云中返回的时间字符串为:"2022-01-25T10:02Z"。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"log"
"time"
)

func main() {
str := "2022-01-25T10:02Z"
t, err := time.Parse("2006-01-02T15:04Z", str)
if err != nil {
log.Fatalln(err)
}
fmt.Println(t)
}

Flag

Flag

Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。

os.Args

如果你只是简单的想要获取命令行参数,可以像下面的代码示例一样使用os.Args来获取命令行参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)

func main() {
// os.Args是一个[]string
if len(os.Args) > 0 {
for i, arg := range os.Args {
fmt.Printf("args[%d]=%v\n", i, arg)
}
}
}

将上面的代码执行go build -o "args_demo.exe"编译之后,生成可执行文件。

执行:args_demo.exe a b c d。结果如下:

因为是在Windows环境下,所以可执行文件是exe。如果在linux环境下,直接go build -o "args_demo"生成可执行文件,然后args_demo a b c d即可。

(如果提示go: go.mod file not found in current directory or any parent directory; see 'go help modules',当然在当前目录或上级目录下执行go mod init 目录名命令即可。)

os.Args是一个存储命令行参数的字符串切片,它的第一个元素是可执行文件的名称。

flag参数类型

flag包支持的命令行参数类型有boolintint64uintuint64floatfloat64stringtime.Duration

flag参数 有效值
字符串flag 合法字符串
整数flag 1234、0664、0x1234等类型,也可以是负数。
浮点数flag 合法浮点数
bool类型flag 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
时间段flag 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。

定义命令行flag参数

有以下两种常用的定义命令行flag参数的方法:

  • flag.Type()
  • flag.TypeVar()

flag.Type()

基本格式:flag.Type(name flag名, value 默认值, usage 备注说明) *Type

例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

1
2
3
4
name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")

需要注意的是,此时name、age、married、delay均为对应类型的指针

flag.String为例,源码如下。可以发现返回的类型是*string

1
2
3
4
5
// String defines a string flag with specified name, default value, and usage string.
// The return value is the address of a string variable that stores the value of the flag.
func String(name string, value string, usage string) *string {
return CommandLine.String(name, value, usage)
}

flag.TypeVar()

基本格式:flag.TypeVar(Type指针, name flag名, value 默认值, usage 备注说明)

例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:

1
2
3
4
5
6
7
8
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")

flag.Parse()

通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。

支持的命令行参数格式有以下几种:

  • -flag xxx (使用空格,一个-符号)
  • --flag xxx (使用空格,两个-符号)
  • -flag=xxx (使用等号,一个-符号)
  • --flag=xxx (使用等号,两个-符号)

其中,布尔类型的参数必须使用等号的方式指定

Flag解析在第一个非flag参数(单个-不是flag参数)之前停止,或者在终止符之后停止。

flag其他函数

  • flag.Args():返回命令行参数后的其他参数,为[]string类型
  • flag.NArg():返回命令行参数后的其他参数个数
  • flag.NFlag():返回使用的命令行参数个数

完整示例

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"flag"
"fmt"
"time"
)

func main() {
// flag.TypeVar方式定义命令行参数
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚姻状况")
flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")

// 解析命令行参数
flag.Parse()
fmt.Println(name, age, married, delay)

fmt.Println(flag.Args()) // 返回命令行参数后的其他参数,为[]string类型
fmt.Println(flag.NArg()) // 返回命令行参数后的其他参数个数
fmt.Println(flag.NFlag()) // 返回使用的命令行参数个数
}

代码编写完成后,执行go build -o "flag_demo"生成可执行文件。

命令行参数使用提示:./flag_demo -help,结果如下:

正常使用命令行flag参数:./flag_demo -name qingbo --age 20 -married=false -d=1h30m,结果如下:

使用非flag命令行参数:./flag_demo a b c,结果如下:

Log

Log

Go语言内置的log包实现了简单的日志服务。

使用logger

log包定义了Logger类型,该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger,可以通过调用函数Print系列(Print|Printf|Println)、Fatal系列Fatal|Fatalf|Fatalln)、和Panic系列Panic|Panicf|Panicln)来使用,比自行创建一个logger对象更容易使用。

例如,我们可以像下面的代码一样直接通过log包来调用上面提到的方法,默认它们会将日志信息打印到终端界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"log"
)

func main() {
log.Println("这是一条很普通的日志。")
v := "很普通的"
log.Printf("这是一条%s日志。\n", v)
log.Fatalln("这是一条会触发fatal的日志。")
log.Panicln("这是一条会触发panic的日志。") // Unreachable code
}

运行结果如下:

可以发现log.Panicln("这是一条会触发panic的日志。")并没有被执行到,因为log.FatallnFatal系列)会调用os.Exit(1)

logger会打印每条日志信息的日期、时间,默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。

配置logger

默认情况下的logger只会提供日志的时间信息,但是很多情况下我们希望得到更多信息,比如记录该日志的文件名和行号等。log标准库中为我们提供了定制这些设置的方法:

  • Flags()函数会返回标准logger的输出配置
  • SetFlags()函数用来设置标准logger的输出配置。
1
2
3
4
5
6
7
8
9
10
11
// Flags returns the output flags for the standard logger.
// The flag bits are Ldate, Ltime, and so on.
func Flags() int {
return std.Flags()
}

// SetFlags sets the output flags for the standard logger.
// The flag bits are Ldate, Ltime, and so on.
func SetFlags(flag int) {
std.SetFlags(flag)
}

flag选项

log标准库提供了如下的flag选项,它们是一系列定义好的常量。

1
2
3
4
5
6
7
8
9
10
11
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期:2009/01/23
Ltime // 时间:01:23:23
Lmicroseconds // 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
Llongfile // 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 文件名+行号:d.go:23(会覆盖掉Llongfile)
LUTC // 使用UTC时间
LstdFlags = Ldate | Ltime // 标准logger的初始值
)

下面我们在记录日志之前先设置一下标准logger的输出选项如下:

1
2
3
4
5
6
7
8
package main

import "log"

func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("一条普通的日志")
}

结果如下:

配置日志前缀

log标准库中还提供了关于日志信息前缀的两个方法:

  • Prefix()函数用来查看标准logger的输出前缀
  • SetPrefix()函数用来设置输出前缀
1
2
3
4
5
6
7
8
9
// Prefix returns the output prefix for the standard logger.
func Prefix() string {
return std.Prefix()
}

// SetPrefix sets the output prefix for the standard logger.
func SetPrefix(prefix string) {
std.SetPrefix(prefix)
}

例子:

1
2
3
4
5
6
7
8
9
10
package main

import "log"

func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("一条普通的日志")
log.SetPrefix("[前缀]")
log.Println("一条普通的日志")
}

结果如下:

这样我们就能够在代码中为我们的日志信息添加指定的前缀,方便之后对日志信息进行检索和处理。

配置日志输出位置

  • SetOutput()函数用来设置标准logger的输出目的地,默认是标准错误输出。
1
2
3
4
5
6
// SetOutput sets the output destination for the standard logger.
func SetOutput(w io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.out = w
}

例如,下面的代码会把日志输出到当前目录下的test.log文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"log"
"os"
)

func main() {
logFile, err := os.OpenFile("./test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalln(err)
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("一条普通的日志")
log.SetPrefix("[前缀]")
log.Println("一条普通的日志")
}

结果如下:

如果你要使用标准的logger,我们通常会把上面的配置操作写到init函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"log"
"os"
)

func main() {
log.Println("一条普通的日志")
log.SetPrefix("[前缀]")
log.Println("一条普通的日志")
}

func init() {
logFile, err := os.OpenFile("./test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalln(err)
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
}

创建logger

log标准库中还提供了一个创建新logger对象的构造函数:New(),支持我们创建自己的logger示例。

1
2
3
4
// New creates a new Logger. The out variable sets the destination to which log data will be written.The prefix appears at the beginning of each generated log line, or after the log header if the Lmsgprefix flag is provided.The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}

New()创建一个Logger对象。其中:

  • 参数out设置日志信息写入的目的地
  • 参数prefix会添加到生成的每一条日志前面
  • 参数flag定义日志的属性(时间、文件等等)

举个例子:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"log"
"os"
)

func main() {
log.New(os.Stdout, "<New>", log.Lshortfile|log.Ldate|log.Ltime)
log.Println("自定义的logger记录的日志")
}

结果如下:

总结 : Go内置的log库功能有限,例如无法满足记录不同级别日志的情况,我们在实际的项目中根据自己的需要选择使用第三方的日志库,如logrus、zap等。

IO操作

IO操作

输入输出的底层原理

终端其实是一个文件,相关实例如下:

  • os.Stdin:标准输入的文件实例,类型为*File
  • os.Stdout:标准输出的文件实例,类型为*File
  • os.Stderr:标准错误输出的文件实例,类型为*File

文件操作相关API

  • func Create(name string) (file *File, err Error):根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666
  • func NewFile(fd uintptr, name string) *File:根据文件描述符创建相应的文件,返回一个文件对象
  • func Open(name string) (file *File, err Error):只读方式打开一个名称为name的文件
  • func OpenFile(name string, flag int, perm uint32) (file *File, err Error):打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限
  • func (file *File) Write(b []byte) (n int, err Error):写入byte类型的信息到文件
  • func (file *File) WriteAt(b []byte, off int64) (n int, err Error):在指定位置开始写入byte类型的信息
  • func (file *File) WriteString(s string) (ret int, err Error):写入string信息到文件
  • func (file *File) Read(b []byte) (n int, err Error):读取数据到b中
  • func (file *File) ReadAt(b []byte, off int64) (n int, err Error):从off开始读取数据到b中
  • func Remove(name string) Error:删除文件名为name的文件

打开和关闭文件

  • os.Open()函数能够打开一个文件,返回一个*File和一个err
  • 对得到的文件实例调用close()方法能够关闭文件

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"log"
"os"
)

func main() {
file, err := os.Open("./hello.txt")
if err != nil {
log.Fatalln(err)
}
file.Close()
}

写文件

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"log"
"os"
)

func main() {
file, err := os.Create("./test.txt") // 新建文件
if err != nil {
log.Fatalln(err)
}
defer file.Close()
for i := 0; i < 3; i++ {
_, err := file.WriteString("hello\n")
_, err = file.Write([]byte("world\n"))
if err != nil {
log.Fatalln(err)
}
}
}

结果:

读文件

文件读取可以用file.Read()file.ReadAt(),读到文件末尾会返回io.EOF的错误

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"io"
"log"
"os"
)

func main() {
file, err := os.Open("./test.txt") // 打开文件
if err != nil {
log.Fatalln(err)
}
defer file.Close()
var buf [128]byte // 定义接收文件读取的字节数组
var content []byte
for {
n, err := file.Read(buf[:])
if err == io.EOF { // 读取结束
break
}
if err != nil {
log.Fatalln(err)
}
content = append(content, buf[:n]...) // ...作用:展开slice。通过append合并两个slice
}
fmt.Println(string(content))
}

结果为:

关于...的用法:函数可变数量参数在go中...有4个用途:

  1. 函数可变数量参数
  2. 展开slice
  3. 定义数组时忽略数组元素数量
  4. go命令:go描述软件包列表时,命令使用...作为通配符。此命令测试当前目录及其子目录的所有软件包。(go test ./...

在这里显然是展开列表的作用。关于展开列表:

1
2
3
4
5
6
7
8
9
func main() {
s := []int{1, 2, 3, 4, 5}
add := []int{9, 9, 9}
fmt.Println(s) // [1 2 3 4 5]
s = append(s, 1)
fmt.Println(s) // [1 2 3 4 5 1]
s = append(s, add...) // s = append(s, add)当然会报错,所以需要Unpack slice
fmt.Println(s) // [1 2 3 4 5 1 9 9 9]
}

拷贝文件

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"io"
"log"
"os"
)

func main() {
srcFile, err := os.Open("./hello.txt") // 打开文件
if err != nil {
log.Fatalln(err)
}
newFile, err := os.Create("./world.txt") // 创建新文件
if err != nil {
log.Fatalln(err)
}
defer srcFile.Close()
defer newFile.Close()
// 缓冲读取
buf := make([]byte, 200)
for {
n, err := srcFile.Read(buf) // 从源文件中读取
if err == io.EOF {
fmt.Println("读取完毕")
break
}
if err != nil {
log.Fatalln(err)
}
// 写出去
_, err = newFile.Write(buf[:n])
if err != nil {
log.Fatalln(err)
}
}
}

bufio

bufio包实现了带缓冲区的读写,是对文件读写的封装。

模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

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

func wr() {
// 参数2:打开模式,所有模式d都在上面
// 参数3是权限控制
// w写 r读 x执行 w 2 r 4 x 1
file, err := os.OpenFile("./xxx.txt", os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return
}
defer file.Close()
// 获取writer对象
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("hello\n")
}
// 刷新缓冲区,强制写出
writer.Flush()
}

func re() {
file, err := os.Open("./xxx.txt")
if err != nil {
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return
}
fmt.Println(string(line))
}

}

func main() {
re()
}

ioutil工具包

  • 工具包写文件
  • 工具包读取文件

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"io/ioutil"
)

func wr() {
err := ioutil.WriteFile("./yyy.txt", []byte("www.5lmh.com"), 0666)
if err != nil {
fmt.Println(err)
return
}
}

func re() {
content, err := ioutil.ReadFile("./yyy.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(content))
}

func main() {
re()
}

实例:实现一个cat命令

使用文件操作相关知识,模拟实现linux平台cat命令的功能。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"bufio"
"flag"
"fmt"
"io"
"os"
)

func main() {
flag.Parse() // 解析命令行参数
if flag.NArg() == 0 { // 如果没有参数默认从标准输入读取内容
cat(bufio.NewReader(os.Stdin))
}
// 依次读取每个指定文件的内容并打印到终端
for i := 0; i < flag.NArg(); i++ {
file, err := os.Open(flag.Arg(i))
if err != nil {
fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
continue
}
cat(bufio.NewReader(file))
}
}

func cat(reader *bufio.Reader) {
for {
bytes, err := reader.ReadBytes('\n') // 注意是字符
if err == io.EOF { // 读取完毕
break
}
fmt.Fprintf(os.Stdout, "%s", bytes)
}
}

接下来go build一下,生成可执行文件:

随便创建一个文件试一下:

Strconv

Strconv

strconv包实现了基本数据类型与其字符串表示的转换,主要有以下常用函数: Atoi()Itia()parse系列、format系列、append系列。

更多函数请查看官方文档

string转int

Atoi()函数用于将字符串类型的整数转换为int类型,函数签名如下:

1
func Atoi(s string) (i int, err error)

如果传入的字符串参数无法转换为int类型,就会返回错误。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"log"
"strconv"
)

func main() {
s1 := "100"
num1, err := strconv.Atoi(s1)
if err != nil {
log.Fatalln(err)
}
fmt.Println(num1) // 100
}

int转string

Itoa()函数用于将int类型数据转换为对应的字符串表示,具体的函数签名如下:

1
func Itoa(i int) string

示例:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
"strconv"
)

func main() {
num1 := 1011
s1 := strconv.Itoa(num1)
fmt.Println(s1) // 1011
}

Parse系列函数:string转其他类型

Parse类函数用于转换字符串为给定类型的值

  • ParseBool()
  • ParseFloat()
  • ParseInt()
  • ParseUint()

ParseBool()

1
func ParseBool(str string) (value bool, err error)

返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。

ParseInt()

1
func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

  • base:指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制
  • bitSize:指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64
  • 返回的err*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange

ParseUnit()

1
func ParseUint(s string, base int, bitSize int) (n uint64, err error)

ParseUint类似ParseInt但不接受正负号,用于无符号整型。

ParseFloat()

1
func ParseFloat(s string, bitSize int) (f float64, err error)

解析一个表示浮点数的字符串并返回其值。如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。

  • bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;
  • 返回值err*NumErr类型的,语法有误的,err.Error=ErrSyntax;结果超出表示范围的,返回值f为±Inferr.Error= ErrRange

代码示例

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"log"
"strconv"
)

func main() {
parseBool, err := strconv.ParseBool("true")
parseInt, err := strconv.ParseInt("-10", 10, 64)
parseUint, err := strconv.ParseUint("11", 10, 64)
parseFloat, err := strconv.ParseFloat("3.1415927", 64)
if err != nil {
log.Fatalln(err)
}
fmt.Println(parseBool) // true
fmt.Println(parseInt) // -10
fmt.Println(parseUint) // 11
fmt.Println(parseFloat) // 3.1415927
}

Format系列函数:其他类型转string

Format系列函数实现了将给定类型数据格式化为string类型数据的功能。

  • FormatBool()
  • FormatInt()
  • FormatUint()
  • FormatFloat()

FormatBool()

1
func FormatBool(b bool) string

根据b的值返回”true”或”false”。

FormatInt()

1
func FormatInt(i int64, base int) string

返回ibase进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母az表示大于10的数字。

FormatUint()

1
func FormatUint(i uint64, base int) string

FormatInt()的无符号整数版本。

FormatFloat()

1
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

函数将浮点数表示为字符串并返回。

  • fmt:表示格式:
    • f(-ddd.dddd)
    • b(-ddddp±ddd,指数为二进制)
    • e(-d.dddde±dd,十进制指数)
    • E(-d.ddddE±dd,十进制指数)
    • g(指数很大时用e格式,否则f格式)
    • G(指数很大时用E格式,否则f格式)
  • prec:控制精度(排除指数部分):
    • feE,它表示小数点后的数字个数
    • gG,它控制总的数字个数
    • 如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f
  • bitSize:表示返回结果的来源类型(32:float32、64:float64),会据此进行舍入。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"strconv"
)

func main() {
s1 := strconv.FormatBool(false)
s2 := strconv.FormatInt(-15, 16) // 返回结果为16进制
s3 := strconv.FormatUint(11, 10)
s4 := strconv.FormatFloat(3.1415926, 'E', -1, 64)
fmt.Println(s1) // false
fmt.Println(s2) // -f
fmt.Println(s3) // 11
fmt.Println(s4) // 3.1415926E+00
}

其他函数

isPrint()

1
func IsPrint(r rune) bool

返回一个字符是否是可打印的,和unicode.IsPrint一样,r必须是:字母(广义)、数字、标点、符号、ASCII空格。

CanBackquote()

1
func CanBackquote(s string) bool

返回字符串s是否可以不被修改的表示为一个单行的没有空格tab之外控制字符的反引号字符串。

其他

除上文列出的函数外,strconv包中还有Append系列、Quote系列等函数。具体用法可查看官方文档

Template

Template

html/template包实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出。它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用text/template包。

在基于MVC的Web架构中,我们通常需要在后端渲染一些数据到HTML文件中,从而实现动态的网页效果。

模板示例

通过将模板应用于一个数据结构(即该数据结构作为模板的参数)来执行,来获得输出。模板中的注释引用数据接口的元素(一般如结构体的字段或者字典的键)来控制执行过程和获取需要呈现的值。模板执行时会遍历结构并将指针表示为.(称之为”dot”)指向运行过程中数据结构的当前位置的值。

用作模板的输入文本必须是utf-8编码的文本。”Action”,数据运算和控制单位,由”“界定;在Action之外的所有文本都不做修改的拷贝到输出中。Action内部不能有换行,但注释可以有换行

HTML文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{.}}</p>
</body>
</html>

Http Server端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"html/template"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Println(err)
return
}
}

func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
teml, err := template.ParseFiles("./template/hello.html")
if err != nil {
log.Println(err)
return
}
// 利用给定数据渲染模板,并将结果写入w
teml.Execute(w, "qingbo1011.top")
}

访问http://localhost:8080/,结果如下:

模板语法

模板语法

当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

HTML代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{.Name}}</p>
<p>性别:{{.Gender}}</p>
<p>年龄:{{.Name}}</p>
</body>
</html>

Http Server端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"html/template"
"log"
"net/http"
)

type UserInfo struct {
Name string
Gender string
Age int
}

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Println(err)
return
}
}

func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./template/hello.html")
if err != nil {
log.Println("create template failed, err:", err)
return
}
// 利用给定数据渲染模板,并将结果写入w
user := UserInfo{
Name: "张三",
Gender: "男",
Age: 18,
}
tmpl.Execute(w, user)
}

访问http://localhost:8080/

同理,当我们传入的变量是map时,也可以在模板文件中通过.来根据key取值。

注释

1
{{/* a comment */}}

​ 注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。

pipeline

模板语法

pipeline是指产生数据的操作。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

注意 : 并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。

变量

Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:

1
$variable := pipeline

其中$variable是变量的名字。声明变量的action不会产生任何输出。

条件判断

Go模板语法中的条件判断有以下几种:

1
2
3
4
5
{{if pipeline}} T1 {{end}}

{{if pipeline}} T1 {{else}} T0 {{end}}

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

1
2
3
4
5
{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。

with

1
2
3
4
5
{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。

预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。

预定义的全局函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
and
函数返回它的第一个empty参数或者最后一个参数;
就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
返回第一个非empty参数或者最后一个参数;
亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
返回它的单个参数的布尔值的否定
len
返回它的参数的整数类型长度
index
执行结果为第一个参数以剩下的参数为索引/键指向的值;
"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
即fmt.Sprint
printf
即fmt.Sprintf
println
即fmt.Sprintln
html
返回其参数文本表示的HTML逸码等价表示。
urlquery
返回其参数文本表示的可嵌入URL查询的逸码等价表示。
js
返回其参数文本表示的JavaScript逸码等价表示。
call
执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
其中Y是函数类型的字段或者字典的值,或者其他类似情况;
call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
该函数类型值必须有12个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

下面是定义为函数的二元比较运算的集合:

1
2
3
4
5
6
eq      如果arg1 == arg2则返回真
ne 如果arg1 != arg2则返回真
lt 如果arg1 < arg2则返回真
le 如果arg1 <= arg2则返回真
gt 如果arg1 > arg2则返回真
ge 如果arg1 >= arg2则返回真

为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:

1
{{eq arg1 arg2 arg3}}

比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。

自定义函数

Go的模板支持自定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func sayHello(w http.ResponseWriter, r *http.Request) {
htmlByte, err := ioutil.ReadFile("./hello.html")
if err != nil {
fmt.Println("read html failed, err:", err)
return
}
// 自定义一个夸人的模板函数
kua := func(arg string) (string, error) {
return arg + "真帅", nil
}
// 采用链式操作在Parse之前调用Funcs添加自定义的kua函数
tmpl, err := template.New("hello").Funcs(template.FuncMap{"kua": kua}).Parse(string(htmlByte))
if err != nil {
fmt.Println("create template failed, err:", err)
return
}

user := UserInfo{
Name: "张三",
Gender: "男",
Age: 18,
}
// 使用user渲染模板,并将结果写入w
tmpl.Execute(w, user)
}

我们可以在模板文件hello.html中使用我们自定义的kua函数了。

1
{{kua .Name}}

嵌套template

我们可以在template中嵌套其他的template。这个template可以是单独的文件,也可以是通过define定义的template。

举个例子: t.html文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>tmpl test</title>
</head>
<body>

<h1>测试嵌套template语法</h1>
<hr>
{{template "ul.html"}}
<hr>
{{template "ol.html"}}
</body>
</html>

{{ define "ol.html"}}
<h1>这是ol.html</h1>
<ol>
<li>吃饱</li>
<li>喝足</li>
<li>唱跳Rap</li>
</ol>
{{end}}

ul.html文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<ul>
<li>注释</li>
<li>日志</li>
<li>测试</li>
</ul>

</body>
</html>

Http Server端如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"html/template"
"log"
"net/http"
)

type UserInfo struct {
Name string
Gender string
Age int
}

func main() {
http.HandleFunc("/tmpl", tmplDemo)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Println(err)
return
}
}

func tmplDemo(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./template/t.html", "./template/ul.html")
if err != nil {
log.Println("create template failed, err:", err)
return
}
user := UserInfo{
Name: "张三",
Gender: "男",
Age: 18,
}
tmpl.Execute(w, user)
}

访问http://localhost:8080/tmpl:

Http

Http

Golang进阶——网络编程 笔记

Go语言内置的net/http包十分的优秀,提供了HTTP客户端服务端的实现。

HTTP协议:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。

HTTP客户端

基本的HTTP/HTTPS请求由 GetHeadPostPostForm函数发出HTTP/HTTPS请求。

1
2
3
4
5
6
resp, err := http.Get("http://qingbo1011.top/")
...
resp, err := http.Post("http://qingbo1011.top/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://5lmh.com/form",
url.Values{"key": {"Value"}, "id": {"123"}})

程序在使用完response后必须Close()

1
2
3
4
5
6
7
resp, err := http.Get("http://5lmh.com/")
if err != nil {
// handle error
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// ...

GET请求示例

使用net/http包编写一个简单的发送HTTP请求的Client端,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

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

func main() {
resp, err := http.Get("https://www.baidu.com/")
if err != nil {
log.Fatalln(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(body))
}

执行上面的代码,就可以在终端输出https://www.baidu.com/网站首页的内容了,我们的浏览器其实就是一个发送和接收HTTP协议数据的客户端,我们平时通过浏览器访问网页其实就是从网站的服务器接收HTTP数据,然后浏览器会按照HTML、CSS等规则将网页渲染展示出来。

带参数的GET请求示例

关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。

Client端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
)

func main() {
apiUrl := "http://127.0.0.1:8080/get"
// URL param
values := url.Values{}
values.Set("name","张三")
values.Set("age","18")
uri, err := url.ParseRequestURI(apiUrl)
if err != nil {
log.Println(err)
}
uri.RawQuery = values.Encode() // URL encode
log.Println(uri.String())
resp, err := http.Get(uri.String())
if err != nil {
log.Println(err)
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
}
fmt.Println(string(bytes))
}

Server端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/get", getHandler) // 单独写回调函数
err := http.ListenAndServe("127.0.0.1:8080", nil) // 参数说明:addr:监听的地址 handler:回调函数
if err != nil {
fmt.Println("err:", err)
return
}
}

func getHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
query := r.URL.Query()
fmt.Println(query.Get("name"))
fmt.Println(query.Get("age"))
response := `{"status": "ok"}`
w.Write([]byte(response))
}

先运行Server端,再运行Client端,结果如下:

Post请求示例

Client端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)

func main() {
url := "http://127.0.0.1:8080/post"
// 表单数据
//contentType := "application/x-www-form-urlencoded"
//data := "name=枯藤&age=18"
// json
contentType := "application/json"
data := `{"name":"张三","age":18}`
resp, err := http.Post(url, contentType, strings.NewReader(data))
if err != nil {
log.Println("post failed, err:%v\n", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("get resp failed,err:%v\n", err)
}
fmt.Println(string(b))
}

Server端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

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

func main() {
http.HandleFunc("/post", postHandler) // 单独写回调函数
err := http.ListenAndServe("127.0.0.1:8080", nil) // 参数说明:addr:监听的地址 handler:回调函数
if err != nil {
fmt.Println("err:", err)
return
}
}

func postHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// 1. 请求类型是application/x-www-form-urlencoded时解析form数据
r.ParseForm()
fmt.Println(r.PostForm) // 打印form数据
fmt.Println(r.PostForm.Get("name"), r.PostForm.Get("age"))
// 2. 请求类型是application/json时从r.Body读取数据
b, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("read request.Body failed, err:%v\n", err)
}
fmt.Println(string(b))
answer := `{"status": "ok"}`
w.Write([]byte(answer))
}

先运行Server端,再运行Client端,结果如下:

自定义Client

要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client:

1
2
3
4
5
6
7
8
9
10
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://5lmh.com")
// ...
req, err := http.NewRequest("GET", "http://5lmh.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...

自定义Transport

要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport:

1
2
3
4
5
6
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://5lmh.com")

Client和Transport类型都可以安全的被多个go程同时使用。出于效率考虑,应该一次建立、尽量重用。

HTTP服务端

默认的Server

ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。

Handle和HandleFunc函数可以向DefaultServeMux添加处理器。

1
2
3
4
5
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))

默认的Server示例

使用Go语言中的net/http包来编写一个简单的接收HTTP请求的Server端示例,net/http包是对net包的进一步封装,专门用来处理HTTP协议的数据。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Printf("http server failed, err:%v\n", err)
return
}
}

func sayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}

运行后访问http://127.0.0.1:8080/,结果如下:

自定义Server

要管理服务端的行为,可以创建一个自定义的Server:

1
2
3
4
5
6
7
8
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())

Context

Context

http包的Server中,每一个请求在都有一个对应的goroutine去处理。请求处理函数通常会启动额外的 goroutine用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源。

为什么需要Context

基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
wg.Add(1)
go hello()
wg.Wait() // 如何优雅的实现结束子goroutine?
fmt.Println("over")

}

func hello() {
defer wg.Done() // 如何接收外部命令实现退出?
for {
fmt.Println("hello!")
time.Sleep(time.Second * 1)
}
}

这个代码运行一下当然是会一直执行,停止不了的。那么如何优雅的实现结束子goroutine呢?

全局变量方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup
var exit bool

func main() {
wg.Add(1)
go hello()
time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
exit = true // 修改全局变量实现子goroutine的退出
wg.Wait()
fmt.Println("over")
}

func hello() {
defer wg.Done() // 如何接收外部命令实现退出?
for {
fmt.Println("hello!")
time.Sleep(time.Second * 1)
if exit {
break
}
}
}

实现效果如下:

全局变量方式存在的问题:

  • 使用全局变量在跨包调用时不容易统一
  • 如果hello()中再启动goroutine,就不太好控制了

通道方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
exitChan := make(chan int)
wg.Add(1)
go hello(exitChan)
time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出
exitChan <- 1 // 给子goroutine发送退出信号
close(exitChan)
wg.Wait()
fmt.Println("over")
}

func hello(exitChan chan int) {
LOOP:
for {
fmt.Println("hello")
time.Sleep(time.Second * 1)
select {
case <-exitChan:
break LOOP
default:
continue
}
}
wg.Done()
}

管道方式存在的问题:

  • 使用全局变量在跨包调用时不容易实现规范和统一
  • 需要维护一个共用的channel

官方版的方案:context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go hello(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}

func hello(ctx context.Context) {
LOOP:
for {
fmt.Println("hello")
time.Sleep(time.Second * 1)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:

}
}
wg.Done()
}

当子goroutine又开启另外一个goroutine时,只需要将ctx传入即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go hello(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}

func hello(ctx context.Context) {
go hello2(ctx)
LOOP:
for {
fmt.Println("hello")
time.Sleep(time.Second * 1)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:

}
}
wg.Done()
}

func hello2(ctx context.Context) {
LOOP:
for {
fmt.Println("hello2")
time.Sleep(time.Second * 1)
select {
case <-ctx.Done(): // 等待上级通知
break LOOP
default:

}
}
wg.Done()
}

Context初识

Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化对于处理单个请求的多个 goroutine之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个API调用。

对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

Context接口

context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • Deadline():需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
  • Done():需要返回一个channel,这个channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done()方法会返回同一个channel;
  • Err():会返回当前Context结束的原因,它只会在Done()返回的channel被关闭时才会返回非空的值;
    • 如果当前Context被取消就会返回Canceled错误;
    • 如果当前Context超时就会返回DeadlineExceeded错误;
  • Value():会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value()并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据。

Background()和TODO()

Go内置两个函数:Background()TODO(),这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

  • Background():主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

  • TODO():目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

    background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

With系列函数

context包中定义了四个With系列函数:WithCancelWithDeadlineWithTimeoutWithValue

WithCancel

WithCancel()的函数签名如下:

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel()返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"context"
"fmt"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}

func gen(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // return结束该goroutine,防止泄露
case dst <- n:
n++
}
}
}()
return dst
}

上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。

WithDeadline

WithDeadline()的函数签名如下:

1
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。

取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"context"
"fmt"
"time"
)

func main() {
d := time.Now().Add(time.Second * 1)
ctx, cancel := context.WithDeadline(context.Background(), d)
// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
defer cancel()
select {
case <-time.After(time.Second * 1):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}

上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文ctx和一个取消函数cancel,然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。 因为ctx 50毫秒后就过期,所以ctx.Done()会先接收到值,上面的代码会打印ctx.Err()取消原因。

WithTimeout

WithTimeout()的函数签名如下:

1
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout()返回WithDeadline(parent, time.Now().Add(timeout))

取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制

具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // 设置一个50毫秒的超时
wg.Add(1)
go connect(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}

func connect(ctx context.Context) {
defer wg.Done()
LOOP:
for {
fmt.Println("db connecting ...")
time.Sleep(time.Millisecond * 10) // // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done():
break LOOP
default:

}
}
fmt.Println("connect done!")
}

WithValue

WithValue()函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:

1
func WithValue(parent Context, key, val interface{}) Context

WithValue()返回父节点的副本,其中与key关联的值为val。

仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。

所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"context"
"fmt"
"sync"
"time"
)

var wg sync.WaitGroup

type TraceCode string // 设置类型别名

func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // 设置一个50毫秒的超时
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
wg.Add(1)
go connect(ctx)
time.Sleep(time.Second * 5)
cancel() // 通知子goroutine结束
wg.Wait()
fmt.Println("over")
}

func connect(ctx context.Context) {
defer wg.Done()
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string)
if !ok {
fmt.Println("invalid trace code")
}
LOOP:
for {
fmt.Printf("worker, trace code:%s\n", traceCode)
time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
select {
case <-ctx.Done():
break LOOP
default:

}
}
fmt.Println("connect done!")
}

使用Context的注意事项

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递

客户端超时取消示例

调用服务端API时如何在客户端实现超时控制?

Server端

server端,随机出现慢响应。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"log"
"math/rand"
"net/http"
"time"
)

func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln(err)
}
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
number := rand.Intn(2) // 生成[0,2)的随机数
if number == 0 {
time.Sleep(time.Second * 3) // 耗时3秒的慢响应
fmt.Println(w, "slow response")
return
}
fmt.Println(w, "quick response")
}

Client端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
"time"
)

type respData struct {
resp *http.Response
err error
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) // 定义一个100毫秒的超时
defer cancel()
doCall(ctx)
}

func doCall(ctx context.Context) {
transport := http.Transport{
// 请求频繁可定义全局的client对象并启用长链接
// 请求不频繁使用短链接
DisableCompression: true,
}
client := http.Client{
Transport: &transport,
}

respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8080/", nil)
if err != nil {
log.Fatalln(err)
}
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
defer wg.Done()
resp, err2 := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err2,
}
respChan <- rd
}()
select {
case <-ctx.Done():
//transport.CancelRequest(req)
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
log.Fatalln("call server api failed, err: ", result.err)
}
defer result.resp.Body.Close()
data, err := ioutil.ReadAll(result.resp.Body)
if err != nil {
log.Fatalln(err)
}
fmt.Println("resp: ", string(data))
}
}

数据格式

数据格式

数据格式是系统中数据交互不可缺少的内容。这里主要介绍JSONXMLMSGPack

JSON

关于json的序列化和反序列化已经用过很多次了。(结构体与JSON序列化 笔记

json是完全独立于语言的文本格式,是k-v的形式。应用场景:前后端交互,系统间数据交互。

json使用go语言内置的encoding/json标准库。

  • 编码json使用json.Marshal()函数可以对一组数据进行JSON格式的编码
  • 解码json使用json.Unmarshal()函数可以对一组数据进行JSON格式的解码
1
2
3
func Marshal(v interface{}) ([]byte, error) 

func Unmarshal(data []byte, v interface{}) error

结构体生成json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

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

// 注意私有变量不能被json包访问!!所以这里的成员全部要大写开头
type student struct {
Id int
Name string
Gender string
}

func main() {
stu := student{
Id: 1001,
Name: "张三",
Gender: "男",
}
// 序列化
bytes, err := json.Marshal(stu)
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(bytes))
// 格式化输出json
jsonBytes, err := json.MarshalIndent(stu, "", " ")
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(jsonBytes))
}

输出结果:

struct tag:

1
2
3
4
5
type Person struct {
//"-"是忽略的意思
Name string `json:"-"`
Hobby string `json:"hobby" `
}

json解析到结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

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

type person struct {
Age int `json:"age,string"`
Name string `json:"name"`
Niubility bool `json:"niubility"`
}

func main() {
b := []byte(`{"age":"18","name":"张三","marry":false}`) // json数据
var p person
err := json.Unmarshal(b, &p)
if err != nil {
log.Fatalln(err)
}
fmt.Println(p) // {18 张三 false}
}

map生成json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

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

func main() {
student := make(map[string]any)
student["name"] = "qingbo1011.top"
student["age"] = 18
student["sex"] = "男"
bytes, err := json.Marshal(student)
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(bytes)) // {"age":18,"name":"qingbo1011.top","sex":"男"}
}

升级到go1.18,使用了any。(any等价于空接口interface{},go1.18中源码的所有空接口都被替换成了any

json解析到any(map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

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

func main() {
// json数据
b := []byte(`{"age":20,"name":"李四","marry":true}`) // int和float64都当float64
var obj any
err := json.Unmarshal(b, &obj) // 自动转到map
if err != nil {
log.Fatalln(err)
}
fmt.Println(obj) // map[age:20 marry:false name:李四]
// 可以判断类型
m, ok := obj.(map[string]any)
if !ok {
log.Println("断言失败!无法断言为map[string]any类型")
}
for k, v := range m {
switch t := v.(type) {
case float64:
fmt.Printf("m[%v]是float64类型\n", k)
case string:
fmt.Printf("m[%v]是string类型\n", k)
default:
fmt.Printf("m[%v]是%v类型\n", k, t)
}
}
}

输出结果:

XML

XML是可扩展标记语言,包含声明、根标签、子元素和属性。应用场景:配置文件以及webService。

示例:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<servers version="1">
<server>
<serverName>Shanghai_VPN</serverName>
<serverIP>127.0.0.1</serverIP>
</server>
<server>
<serverName>Beijing_VPN</serverName>
<serverIP>127.0.0.2</serverIP>
</server>
</servers>

XML 笔记

直接看代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

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

type Server struct {
ServerName string `xml:"serverName"`
ServerIP string `xml:"serverIP"`
}

type Servers struct {
Name xml.Name `xml:"servers"`
Version string `xml:"version"`
Servers []Server `xml:"server"`
}

func main() {
// xml转struct
xmlStr := `
<?xml version="1.0" encoding="UTF-8" ?>
<servers version="1">
<server>
<serverName>Shanghai_VPN</serverName>
<serverIP>127.0.0.1</serverIP>
</server>
<server>
<serverName>Beijing_VPN</serverName>
<serverIP>127.0.0.2</serverIP>
</server>
</servers>
`
var servers Servers
err := xml.Unmarshal([]byte(xmlStr), &servers)
if err != nil {
log.Fatalln(err)
}
fmt.Println(servers) // {{ } [{Shanghai_VPN 127.0.0.1} {Beijing_VPN 127.0.0.2}]}
}

MSGPack

MSGPack是二进制的json,性能更快,更省空间。

It’s like JSON. but fast and small.

MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it’s faster and smaller. Small integers are encoded into a single byte, and typical short strings require only one extra byte in addition to the strings themselves.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import (
"fmt"
"github.com/vmihailenco/msgpack"
"io/ioutil"
"math/rand"
)

type Person struct {
Name string
Age int
Sex string
}

// 二进制写出
func writerJson(filename string) (err error) {
var persons []*Person
// 假数据
for i := 0; i < 10; i++ {
p := &Person{
Name: fmt.Sprintf("name%d", i),
Age: rand.Intn(100),
Sex: "male",
}
persons = append(persons, p)
}
// 二进制json序列化
data, err := msgpack.Marshal(persons)
if err != nil {
fmt.Println(err)
return
}
err = ioutil.WriteFile(filename, data, 0666)
if err != nil {
fmt.Println(err)
return
}
return
}

// 二进制读取
func readJson(filename string) (err error) {
var persons []*Person
// 读文件
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println(err)
return
}
// 反序列化
err = msgpack.Unmarshal(data, &persons)
if err != nil {
fmt.Println(err)
return
}
for _, v := range persons {
fmt.Printf("%#v\n", v)
}
return
}

func main() {
//err := writerJson("D:/person.dat")
//if err != nil {
// fmt.Println(err)
// return
//}
err := readJson("D:/person.dat")
if err != nil {
fmt.Println(err)
return
}
}

反射

反射

Go语言设计与实现之反射

反射是指在程序运行期对程序本身进行访问和修改的能力

变量的内在机制

1
2
var arr [10]int
arr[0] = 10

变量包含类型信息值信息

  • 类型信息:是静态的元信息,是预先定义好的
  • 值信息:是程序运行过程中动态改变的

反射的使用

reflect包封装了反射相关的方法:

  • 获取类型信息:reflect.TypeOf(),是静态的
  • 获取值信息:reflect.ValueOf(),是动态的

空接口与反射

反射可以在运行时动态获取程序的各种详细信息。

反射获取interface类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.14
test(x)
}

func test(a any) {
t := reflect.TypeOf(a)
fmt.Println(t) // float64
// kind()可以获取具体类型
k := t.Kind()
fmt.Println(k) // float64
switch k {
case reflect.Float64:
fmt.Println("float64")
case reflect.String:
fmt.Println("string")
default:
fmt.Println("其他类型")
}
/* switch t2 := a.(type) {
case float64:
fmt.Println(t2, "float64") // 3.14 float64
case string:
fmt.Println(t2, "string")
default:
fmt.Println(t2, "其他类型")
}*/
}

关于Kind,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Pointer
Slice
String
Struct
UnsafePointer
)

反射获取interface值信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.14
test(x)
}

func test(a any) {
v := reflect.ValueOf(a)
fmt.Println(v) // 3.14
k := v.Kind()
fmt.Println(k) // float64
switch k {
case reflect.Float64:
fmt.Println("a是:", v.Float()) // a是: 3.14
}
}

反射修改值信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.14
fmt.Println(x) // 3.14
// 反射认为下面是指针类型,不是float64类型
reflectSetValue(&x)
fmt.Println(x) // 8.8
}

func reflectSetValue(a any) {
v := reflect.ValueOf(a)
k := v.Kind()
switch k {
case reflect.Float64:
// 反射修改值
v.SetFloat(6.18)
fmt.Println("a is Float64", v.Float())
case reflect.Ptr:
v.Elem().SetFloat(8.8) // Elem()获取地址指向的值
fmt.Println("a is Ptr:", v.Elem().Float()) // a is Ptr: 8.8
// 地址
fmt.Println(v.Pointer()) // 824634417240
}
}

结构体与反射

查看类型、字段和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"fmt"
"reflect"
)

type user struct {
Id int
Name string
Age int
}

func (u user) Hello() {
fmt.Println(u.Name, "say: hello!")
}

func main() {
u := user{
Id: 1,
Name: "张三",
Age: 18,
}
poni(u)
}

func poni(a any) {
t := reflect.TypeOf(a)
fmt.Println("类型:", t) // 类型: main.user
fmt.Println("以结构体字符串形式输出:", t.Name()) // 以结构体字符串形式输出: user
// 获取值
v := reflect.ValueOf(a)
fmt.Println(v) // {1 张三 18}
// 获取所有属性
for i := 0; i < t.NumField(); i++ { // t.NumField():获取结构体字段个数
// 取每个字段
f := t.Field(i)
fmt.Printf("%s : %v\n", f.Name, f.Type)
// 获取字段的值信息
val := v.Field(i).Interface() // Interface():获取字段对应的值
fmt.Println("val :", val)
}
fmt.Println("=======================================")
// 获取方法
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(m.Name) // Hello
fmt.Println(m.Type) // func(main.user)
}
}

输出结果:

查看匿名字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

type Boy struct {
User
Addr string
}

func main() {
m := Boy{
User: User{1, "张三", 18},
Addr: "北京",
}
t := reflect.TypeOf(m)
fmt.Println(t) // main.Boy
// Anonymous:匿名
fmt.Printf("%#v\n", t.Field(0)) // reflect.StructField{Name:"User", PkgPath:"", Type:(*reflect.rtype)(0x3bd400), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true}
// 值信息
fmt.Printf("%#v\n", reflect.ValueOf(m).Field(0)) // main.User{Id:1, Name:"张三", Age:18}
}

修改结构体的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func main() {
u := User{
Id: 0,
Name: "张三",
Age: 18,
}
fmt.Println(u) // {0 张三 18}
SetValue(&u)
fmt.Println(u) // {0 李四 18}
}

func SetValue(a any) {
v := reflect.ValueOf(a)
value := v.Elem() // 获取指针指向的元素
field := value.FieldByName("Name") // 取字段
if field.Kind() == reflect.String {
field.SetString("李四")
}
}

调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) Hello(name string) {
fmt.Println(u.Name, "say: Hello!", name)
}

func main() {
u := User{
Id: 1,
Name: "张三",
Age: 18,
}
v := reflect.ValueOf(u)
m := v.MethodByName("Hello") // 获取方法
args := []reflect.Value{reflect.ValueOf("李四")} // 构建一些参数
m.Call(args) // 调用方法,需要传入方法的参数
// 没参数的情况下:
//var args2 []reflect.Value
}

输出结果:

1
张三 say: Hello! 李四

获取字段的tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"reflect"
)

type Student struct {
Name string `json:"name" db:"stu_name"`
}

func main() {
var s Student
v := reflect.ValueOf(&s)
t := v.Type() // 类型
field := t.Elem().Field(0) // 获取字段
fmt.Println(field.Tag.Get("json")) // name
fmt.Println(field.Tag.Get("db")) // stu_name

}