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 mainimport "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 mainimport ( "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 mainimport "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
打印值的类型
%%
百分号
布尔型
整型
占位符
说明
%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 mainimport "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.Scan
、fmt.Scanf
、fmt.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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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.Scan
、fmt.Scanf
、fmt.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.Scan
、fmt.Scanf
、fmt.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 mainimport ( "fmt" "time" ) func main () { now := time.Now() fmt.Printf("current time:%v\n" , now) 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) }
时间戳
时间戳 是自1970年1月1日(08:00:00GMT)至当前时间的 总毫秒数 。它也被称为Unix时间戳(UnixTimestamp)。
基于时间对象获取时间戳的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "time" ) func main () { now := time.Now() timestamp1 := now.Unix() timestamp2 := now.UnixNano() fmt.Printf("current timestamp1:%v\n" , timestamp1) fmt.Printf("current timestamp2:%v\n" , timestamp2) }
使用time.Unix()
函数可以将时间戳转为时间格式:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" "time" ) func main () { timestamp := int64 (1656341407 ) t := time.Unix(timestamp, 0 ) fmt.Println(t) }
时间间隔
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 mainimport ( "fmt" "time" ) func main () { now := time.Now() later := now.Add(time.Hour) fmt.Println(later) }
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 mainimport ( "fmt" "time" ) func main () { ticker := time.Tick(time.Second * 2 ) for i := range ticker { fmt.Println(i) } }
时间格式化
时间类型有一个自带的方法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 mainimport ( "fmt" "time" ) func main () { now := time.Now() fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan" )) fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan" )) fmt.Println(now.Format("2006/01/02 15:04" )) fmt.Println(now.Format("15:04 2006/01/02" )) fmt.Println(now.Format("2006/01/02" )) }
解析字符串格式的时间
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 mainimport ( "fmt" "log" "time" ) func main () { now := time.Now() fmt.Println(now) 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) fmt.Println(t.Sub(now)) }
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 mainimport ( "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 mainimport ( "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 mainimport ( "fmt" "os" ) func main () { 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包支持的命令行参数类型有bool
、int
、int64
、uint
、uint64
、float
、float64
、string
、time.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 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.Durationflag.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 mainimport ( "flag" "fmt" "time" ) func main () { 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()) 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 mainimport ( "log" ) func main () { log.Println("这是一条很普通的日志。" ) v := "很普通的" log.Printf("这是一条%s日志。\n" , v) log.Fatalln("这是一条会触发fatal的日志。" ) log.Panicln("这是一条会触发panic的日志。" ) }
运行结果如下:
、
可以发现log.Panicln("这是一条会触发panic的日志。")
并没有被执行到,因为log.Fatalln
(Fatal系列 )会调用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 func Flags () int { return std.Flags() } func SetFlags (flag int ) { std.SetFlags(flag) }
flag选项
log
标准库提供了如下的flag选项,它们是一系列定义好的常量。
1 2 3 4 5 6 7 8 9 10 11 const ( Ldate = 1 << iota Ltime Lmicroseconds Llongfile Lshortfile LUTC LstdFlags = Ldate | Ltime )
下面我们在记录日志之前先设置一下标准logger的输出选项如下:
1 2 3 4 5 6 7 8 package mainimport "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 func Prefix () string { return std.Prefix() } func SetPrefix (prefix string ) { std.SetPrefix(prefix) }
例子:
1 2 3 4 5 6 7 8 9 10 package mainimport "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 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 mainimport ( "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 mainimport ( "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 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 mainimport ( "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 mainimport ( "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 mainimport ( "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 mainimport ( "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]...) } fmt.Println(string (content)) }
结果为:
关于...
的用法:函数可变数量参数在go中...
有4个用途:
函数可变数量参数
展开slice
定义数组时忽略数组元素数量
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) s = append (s, 1 ) fmt.Println(s) s = append (s, add...) fmt.Println(s) }
拷贝文件
代码如下:
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 mainimport ( "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 mainimport ( "bufio" "fmt" "io" "os" ) func wr () { file, err := os.OpenFile("./xxx.txt" , os.O_CREATE|os.O_WRONLY, 0666 ) if err != nil { return } defer file.Close() 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 mainimport ( "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 mainimport ( "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 mainimport ( "fmt" "log" "strconv" ) func main () { s1 := "100" num1, err := strconv.Atoi(s1) if err != nil { log.Fatalln(err) } fmt.Println(num1) }
int转string
Itoa()
函数用于将int类型数据转换为对应的字符串表示,具体的函数签名如下:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" "strconv" ) func main () { num1 := 1011 s1 := strconv.Itoa(num1) fmt.Println(s1) }
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为±Inf
,err.Error= ErrRange
。
代码示例
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "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) fmt.Println(parseInt) fmt.Println(parseUint) fmt.Println(parseFloat) }
Format系列函数实现了将给定类型数据格式化为string类型数据 的功能。
FormatBool()
FormatInt()
FormatUint()
FormatFloat()
1 func FormatBool (b bool ) string
根据b的值返回”true”或”false”。
1 func FormatInt (i int64 , base int ) string
返回i
的base
进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母a
到z
表示大于10的数字。
1 func FormatUint (i uint64 , base int ) string
FormatInt()
的无符号整数版本。
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
:控制精度(排除指数部分):
对f
、e
、E
,它表示小数点后的数字个数
对g
、G
,它控制总的数字个数
如果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 mainimport ( "fmt" "strconv" ) func main () { s1 := strconv.FormatBool(false ) s2 := strconv.FormatInt(-15 , 16 ) s3 := strconv.FormatUint(11 , 10 ) s4 := strconv.FormatFloat(3.1415926 , 'E' , -1 , 64 ) fmt.Println(s1) fmt.Println(s2) fmt.Println(s3) fmt.Println(s4) }
其他函数
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 mainimport ( "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 } 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 mainimport ( "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 } user := UserInfo{ Name: "张三" , Gender: "男" , Age: 18 , } tmpl.Execute(w, user) }
访问http://localhost:8080/
:
同理,当我们传入的变量是map
时,也可以在模板文件中通过.
来根据key
取值。
注释
注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
pipeline
模板语法
pipeline是指产生数据的操作。Go的模板语法中支持使用管道符号|
链接多个命令,用法和unix下的管道类似:|
前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。
注意 : 并不是只有使用了|
才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。
变量
Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:
其中$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 明显不同); 该函数类型值必须有1 到2 个返回值,如果有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个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:
比较函数只适用于基本类型(或重定义的基本类型,如”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 } 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 , } tmpl.Execute(w, user) }
我们可以在模板文件hello.html中使用我们自定义的kua函数了。
嵌套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 mainimport ( "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请求由 Get
、Head
、Post
和PostForm
函数发出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 { } 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 mainimport ( "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 mainimport ( "fmt" "io/ioutil" "log" "net/http" "net/url" ) func main () { apiUrl := "http://127.0.0.1:8080/get" 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() 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 mainimport ( "fmt" "net/http" ) func main () { http.HandleFunc("/get" , getHandler) err := http.ListenAndServe("127.0.0.1:8080" , nil ) 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 mainimport ( "fmt" "io/ioutil" "log" "net/http" "strings" ) func main () { url := "http://127.0.0.1:8080/post" 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 mainimport ( "fmt" "io/ioutil" "log" "net/http" ) func main () { http.HandleFunc("/post" , postHandler) err := http.ListenAndServe("127.0.0.1:8080" , nil ) if err != nil { fmt.Println("err:" , err) return } } func postHandler (w http.ResponseWriter, r *http.Request) { defer r.Body.Close() r.ParseForm() fmt.Println(r.PostForm) fmt.Println(r.PostForm.Get("name" ), r.PostForm.Get("age" )) 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 mainimport ( "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 mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { wg.Add(1 ) go hello() wg.Wait() 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 mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupvar exit bool func main () { wg.Add(1 ) go hello() time.Sleep(time.Second * 3 ) exit = true 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 mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { exitChan := make (chan int ) wg.Add(1 ) go hello(exitChan) time.Sleep(time.Second * 3 ) exitChan <- 1 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 mainimport ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1 ) go hello(ctx) time.Sleep(time.Second * 3 ) cancel() 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 mainimport ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1 ) go hello(ctx) time.Sleep(time.Second * 3 ) cancel() 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调用。
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancel
、WithDeadline
、WithTimeout
或WithValue
创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
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系列函数:WithCancel
、WithDeadline
、WithTimeout
和WithValue
。
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 mainimport ( "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 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 mainimport ( "context" "fmt" "time" ) func main () { d := time.Now().Add(time.Second * 1 ) ctx, cancel := context.WithDeadline(context.Background(), d) 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 mainimport ( "context" "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) wg.Add(1 ) go connect(ctx) time.Sleep(time.Second * 5 ) cancel() wg.Wait() fmt.Println("over" ) } func connect (ctx context.Context) { defer wg.Done() LOOP: for { fmt.Println("db connecting ..." ) time.Sleep(time.Millisecond * 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 mainimport ( "context" "fmt" "sync" "time" ) var wg sync.WaitGrouptype TraceCode string func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) ctx = context.WithValue(ctx, TraceCode("TRACE_CODE" ), "12512312234" ) wg.Add(1 ) go connect(ctx) time.Sleep(time.Second * 5 ) cancel() 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 ) 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 mainimport ( "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 ) if number == 0 { time.Sleep(time.Second * 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 mainimport ( "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 ) defer cancel() doCall(ctx) } func doCall (ctx context.Context) { transport := http.Transport{ 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(): 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)) } }
数据格式
数据格式
数据格式是系统中数据交互不可缺少的内容。这里主要介绍JSON
、XML
、MSGPack
。
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 mainimport ( "encoding/json" "fmt" "log" ) 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)) 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 mainimport ( "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}` ) var p person err := json.Unmarshal(b, &p) if err != nil { log.Fatalln(err) } fmt.Println(p) }
map生成json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "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)) }
升级到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 mainimport ( "encoding/json" "fmt" "log" ) func main () { b := []byte (`{"age":20,"name":"李四","marry":true}` ) var obj any err := json.Unmarshal(b, &obj) if err != nil { log.Fatalln(err) } fmt.Println(obj) 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 mainimport ( "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 () { 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) }
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 mainimport ( "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) } 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 := 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 mainimport ( "fmt" "reflect" ) func main () { var x float64 = 3.14 test(x) } func test (a any) { t := reflect.TypeOf(a) fmt.Println(t) k := t.Kind() fmt.Println(k) switch k { case reflect.Float64: fmt.Println("float64" ) case reflect.String: fmt.Println("string" ) default : fmt.Println("其他类型" ) } }
关于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 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 mainimport ( "fmt" "reflect" ) func main () { var x float64 = 3.14 test(x) } func test (a any) { v := reflect.ValueOf(a) fmt.Println(v) k := v.Kind() fmt.Println(k) switch k { case reflect.Float64: fmt.Println("a是:" , v.Float()) } }
反射修改值信息
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 mainimport ( "fmt" "reflect" ) func main () { var x float64 = 3.14 fmt.Println(x) reflectSetValue(&x) fmt.Println(x) } 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 ) fmt.Println("a is Ptr:" , v.Elem().Float()) fmt.Println(v.Pointer()) } }
结构体与反射
查看类型、字段和方法
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 mainimport ( "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) fmt.Println("以结构体字符串形式输出:" , t.Name()) v := reflect.ValueOf(a) fmt.Println(v) for i := 0 ; i < t.NumField(); i++ { f := t.Field(i) fmt.Printf("%s : %v\n" , f.Name, f.Type) val := v.Field(i).Interface() fmt.Println("val :" , val) } fmt.Println("=======================================" ) for i := 0 ; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Println(m.Name) fmt.Println(m.Type) } }
输出结果:
查看匿名字段
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 mainimport ( "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) fmt.Printf("%#v\n" , t.Field(0 )) fmt.Printf("%#v\n" , reflect.ValueOf(m).Field(0 )) }
修改结构体的值
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 mainimport ( "fmt" "reflect" ) type User struct { Id int Name string Age int } func main () { u := User{ Id: 0 , Name: "张三" , Age: 18 , } fmt.Println(u) SetValue(&u) fmt.Println(u) } 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 mainimport ( "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) }
输出结果:
获取字段的tag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "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" )) fmt.Println(field.Tag.Get("db" )) }