第五章:学标准库
1 fmt.Printf 方法速查指南
1.1 fmt 的三大函数对比
fmt 标准库是我们在学习和编写 Go 代码,使用最频繁的库之一。
在新手阶段,通常会使用 fmt 包的 打印函数来查看变量的信息。
这样的打印函数,有三个
- fmt.Print:正常打印字符串和变量,不会进行格式化,不会自动换行,需要手动添加 \n 进行换行,多个变量值之间不会添加空格
- fmt.Println:正常打印字符串和变量,不会进行格式化,多个变量值之间会添加空格,并且在每个变量值后面会进行自动换行
- fmt.Printf:可以按照自己需求对变量进行格式化打印。需要手动添加 \n 进行换行
func main() {
fmt.Print("hello", "world\n")
fmt.Println("hello", "world")
fmt.Printf("hello world\n")
}
输出如下
helloworld
hello world
hello world
前面两个函数,使用起来比较简单,容易上手。
而第三个函数,使用起来虽然灵活,却有一定的上手难度,想要完全掌握,需要不断的进行练习。
因此,我花了半天的时间,参考官方文档,对 fmt.Printf 的使用进行了系统学习,整理了这篇文章。
这篇文章足够全面,完全可以成为你在使用 fmt.Printf 时的中文手册,收藏起来,需要用到了就来查一查。
1.2 初识 fmt.Prinf 函数
Printf 函数的定义如下
func Printf(format string, a ...interface{}) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
它的 第一个参数是需要格式化的字符串,这个字符串可以是不包含占位符的字符串,也可以是包含占位符的字符串。
占位符 是以 % 开头的 n 位短代码,这些短代码根据约定的格式决定着变量输出的格式。
先举个例子
我想知道10 进制的 1024 用 2 进制、8进制、16进制表示各是什么?
可以像下面这样子写,其中的 %d、%b、%o、%x 都是叫做占位符,它决定了要以怎样的形式打印后面的变量 n。
package main
import "fmt"
func main() {
n := 1024
fmt.Printf("%d 的 2 进制:%b \n", n, n)
fmt.Printf("%d 的 8 进制:%o \n", n, n)
fmt.Printf("%d 的 10 进制:%d \n", n, n)
fmt.Printf("%d 的 16 进制:%x \n", n, n)
}
运行后,输出如下
1024 的 2 进制:10000000000
1024 的 8 进制:2000
1024 的 10 进制:1024
1024 的 16 进制:400
初步理解了它的运行原理后,接下我会详细的讲解 fmt.Printf中的占位符都有哪些,他们各表示着什么意思。
1.3 详解 Printf 的占位符
通用占位符
- %v:以值的默认格式打印
- %+v:类似%v,但输出结构体时会添加字段名
- %#v:值的Go语法表示
- %T:打印值的类型
- %%: 打印百分号本身
type Profile struct {
name string
gender string
age int
}
func main() {
var people = Profile{name:"wangbm", gender: "male", age:27}
fmt.Printf("%v \n", people) // output: {wangbm male 27}
fmt.Printf("%T \n", people) // output: main.Profile
// 打印结构体名和类型
fmt.Printf("%#v \n", people) // output: main.Profile{name:"wangbm", gender:"male", age:27}
fmt.Printf("%+v \n", people) // output: {name:wangbm gender:male age:27}
fmt.Printf("%% \n") // output: %
}
运行后、输出如下
{wangbm male 27}
main.Profile
main.Profile{name:"wangbm", gender:"male", age:27}
{name:wangbm gender:male age:27}
%
打印布尔值
func main() {
fmt.Printf("%t \n", true) //output: true
fmt.Printf("%t \n", false) //output: false
}
打印字符串
- %s:输出字符串表示(string类型或[]byte)
- %q:双引号围绕的字符串,由Go语法安全地转义
- %x:十六进制,小写字母,每字节两个字符
- %X:十六进制,大写字母,每字节两个字符
func main() {
fmt.Printf("%s \n", []byte("Hello, Golang")) // output: Hello, Golang
fmt.Printf("%s \n", "Hello, Golang") // output: Hello, Golang
fmt.Printf("%q \n", []byte("Hello, Golang")) // output: "Hello, Golang"
fmt.Printf("%q \n", "Hello, Golang") // output: "Hello, Golang"
fmt.Printf("%q \n", `hello \r\n world`) // output: "hello \\r\\n world"
fmt.Printf("%x \n", "Hello, Golang") // output: 48656c6c6f2c20476f6c616e67
fmt.Printf("%X \n", "Hello, Golang") // output: 48656c6c6f2c20476f6c616e67
}
运行后、输出如下
Hello, Golang
Hello, Golang
"Hello, Golang"
"Hello, Golang"
"hello \\r\\n world"
48656c6c6f2c20476f6c616e67
48656C6C6F2C20476F6C616E67
打印指针
func main() {
var people = Profile{name:"wangbm", gender: "male", age:27}
fmt.Printf("%p", &people) // output: 0xc0000a6150
}
打印整型
- %b:以二进制打印
- %d:以十进制打印
- %o:以八进制打印
- %x:以十六进制打印,使用小写:a-f
- %X:以十六进制打印,使用大写:A-F
- %c:打印对应的的unicode码值
- %q:该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
- %U:该值对应的 Unicode格式:U+1234,等价于”U+%04X”
func main() {
n := 1024
fmt.Printf("%d 的 2 进制:%b \n", n, n)
fmt.Printf("%d 的 8 进制:%o \n", n, n)
fmt.Printf("%d 的 10 进制:%d \n", n, n)
fmt.Printf("%d 的 16 进制:%x \n", n, n)
// 将 10 进制的整型转成 16 进制打印: %x 为小写, %X 为小写
fmt.Printf("%x \n", 1024)
fmt.Printf("%X \n", 1024)
// 根据 Unicode码值打印字符
fmt.Printf("ASCII 编码为%d 表示的字符是: %c \n", 65, 65) // output: A
// 根据 Unicode 编码打印字符
fmt.Printf("%c \n", 0x4E2D) // output: 中
// 打印 raw 字符时
fmt.Printf("%q \n", 0x4E2D) // output: '中'
// 打印 Unicode 编码
fmt.Printf("%U \n", '中') // output: U+4E2D
}
运行后,输出如下
1024 的 2 进制:10000000000
1024 的 8 进制:2000
1024 的 10 进制:1024
1024 的 16 进制:400
400
400
ASCII 编码为65 表示的字符是: A
中
'中'
U+4E2D
打印浮点数
- %e:科学计数法,如-1234.456e+78
- %E:科学计数法,如-1234.456E+78
- %f:有小数部分但无指数部分,如123.456
- %F:等价于%f
- %g:根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
- %G:根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
func main() {
f := 12.34
fmt.Printf("%b\n", f)
fmt.Printf("%e\n", f)
fmt.Printf("%E\n", f)
fmt.Printf("%f\n", f)
fmt.Printf("%g\n", f)
fmt.Printf("%G\n", f)
}
输出如下
6946802425218990p-49
1.234000e+01
1.234000E+01
12.340000
12.34
12.34
宽度标识符
宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。
如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:
func main() {
n := 12.34
fmt.Printf("%f\n", n) // 以默认精度打印
fmt.Printf("%9f\n", n) // 宽度为9,默认精度
fmt.Printf("%.2f\n", n) // 默认宽度,精度2
fmt.Printf("%9.2f\n", n) //宽度9,精度2
fmt.Printf("%9.f\n", n) // 宽度9,精度0
}
输出如下
10.240000
10.240000
10.24
10.24
10
占位符:%+
- %+v:若值为结构体,则输出将包括结构体的字段名。
- %+q:保证只输出ASCII编码的字符,非 ASCII 字符则以unicode编码表示
func main() {
// 若值为结构体,则输出将包括结构体的字段名。
var people = Profile{name:"wangbm", gender: "male", age:27}
fmt.Printf("%v \n", people) // output: {name:wangbm gender:male age:27}
fmt.Printf("%+v \n", people) // output: {name:wangbm gender:male age:27}
// 保证只输出ASCII编码的字符
fmt.Printf("%q \n", "golang") // output: "golang"
fmt.Printf("%+q \n", "golang") // output: "golang"
// 非 ASCII 字符则以unicode编码表示
fmt.Printf("%q \n", "中文") // output: "中文"
fmt.Printf("%+q \n", "中文") // output: "\u4e2d\u6587"
}
输出如下
{wangbm male 27}
{name:wangbm gender:male age:27}
"golang"
"golang"
"中文"
"\u4e2d\u6587"
占位符:%
- %#x:给打印出来的是 16 进制字符串加前缀 0x
- %#q:用反引号包含,打印原始字符串
- %#U:若是可打印的字符,则将其打印出来
- %#p:若是打印指针的内存地址,则去掉前缀 0x
对齐补全
字符串
func main() {
// 对于打印出来的是 16 进制,则加前缀 0x
fmt.Printf("%x \n", "Hello, Golang") // output: 48656c6c6f2c20476f6c616e67
fmt.Printf("%#x \n", "Hello, Golang") // output: 0x48656c6c6f2c20476f6c616e67
// 用反引号包含,打印原始字符串
fmt.Printf("%q \n", "Hello, Golang") // output: "Hello, Golang"
fmt.Printf("%#q \n", "Hello, Golang") // output: `Hello, Golang`
// 若是可打印的字符,则将其打印出来
fmt.Printf("%U \n", '中') // output: U+4E2D
fmt.Printf("%#U \n", '中') // output: U+4E2D '中'
// 若是打印指针的内存地址,则去掉前缀 0x
a := 1024
fmt.Printf("%p \n", &a) // output: 0xc0000160e0
fmt.Printf("%#p \n", &a) // output: c0000160e0
}
func main() {
// 打印的值宽度为5,若不足5个字符,则在前面补空格凑足5个字符。
fmt.Printf("a%5sc\n", "b") // output: a bc
// 打印的值宽度为5,若不足5个字符,则在后面补空格凑足5个字符。
fmt.Printf("a%-5sc\n", "b") //output: ab c
// 不想用空格补全,还可以指定0,其他数值不可以,注意:只能在前边补全,后边补全无法指定字符
fmt.Printf("a%05sc\n", "b") // output: a0000bc
// 若超过5个字符,不会截断
fmt.Printf("a%5sd\n", "bbbccc") // output: abbbcccd
}
输出如下
a bc
ab c
a0000bc
abbbcccd
浮点数
func main() {
// 保证宽度为6(包含小数点),2位小数,右对齐
// 不足6位时,整数部分空格补全,小数部分补零,超过6位时,小数部分四舍五入
fmt.Printf("%6.2f,%6.2f\n", 12.3, 123.4567)
// 保证宽度为6(包含小数点),2位小数,- 表示左对齐
// 不足6位时,整数部分空格补全,小数部分补零,超过6位时,小数部分四舍五入
fmt.Printf("%-6.2f,%-6.2f\n", 12.2, 123.4567)
}
输出如下
12.30,123.46
12.20 ,123.46
正负号占位
如果是正数,则留一个空格,表示正数
如果是负数,则在此位置,用 - 表示
func main() {
fmt.Printf("1% d3\n", 22)
fmt.Printf("1% d3\n", -22)
}
输出如下
1 223
1-223
以上就是参考 golang - fmt 文档 整理而成的 fmt.Printf 的使用手册。
2 os/exec 执行命令的五种姿势
在 Golang 中用于执行命令的库是 os/exec,exec.Command 函数返回一个 Cmd 对象,根据不同的需求,可以将命令的执行分为三种情况
- 只执行命令,不获取结果
- 执行命令,并获取结果(不区分 stdout 和 stderr)
- 执行命令,并获取结果(区分 stdout 和 stderr)
第一种:只执行命令,不获取结果
直接调用 Cmd 对象的 Run 函数,返回的只有成功和失败,获取不到任何输出的结果。
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-l", "/var/log/")
err := cmd.Run()
if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err)
}
}
第二种:执行命令,并获取结果
有时候我们执行一个命令就是想要获取输出结果,此时你可以调用 Cmd 的 CombinedOutput 函数。
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-l", "/var/log/")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}
CombinedOutput 函数,只返回 out,并不区分 stdout 和 stderr。如果你想区分他们,可以直接看第三种方法。
$ go run demo.go
combined out:
total 11540876
-rw-r--r-- 2 root root 4096 Oct 29 2018 yum.log
drwx------ 2 root root 94 Nov 6 05:56 audit
-rw-r--r-- 1 root root 185249234 Nov 28 2019 message
-rw-r--r-- 2 root root 16374 Aug 28 10:13 boot.log
不过在那之前,我却发现一个小问题:有时候,shell 命令能执行,并不代码 exec 也能执行。
比如我只想查看 /var/log/ 目录下的 log 后缀名的文件呢?有点 Linux 基础的同学,都会用这个命令
$ ls -l /var/log/*.log
total 11540
-rw-r--r-- 2 root root 4096 Oct 29 2018 /var/log/yum.log
-rw-r--r-- 2 root root 16374 Aug 28 10:13 /var/log/boot.log
按照这个写法将它放入到 exec.Command
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-l", "/var/log/*.log")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}
什么情况?居然不行,报错了。
$ go run demo.go
combined out:
ls: cannot access /var/log/*.log: No such file or directory
2020/11/11 19:46:00 cmd.Run() failed with exit status 2
exit status 1
为什么会报错呢?shell 明明没有问题啊
其实很简单,原来 ls -l /var/log/*.log 并不等价于下面这段代码。
exec.Command("ls", "-l", "/var/log/*.log")
上面这段代码对应的 Shell 命令应该是下面这样,如果你这样子写,ls 就会把参数里的内容当成具体的文件名,而忽略通配符 *
$ ls -l "/var/log/*.log"
ls: cannot access /var/log/*.log: No such file or directory
第三种:执行命令,并区分stdout 和 stderr
上面的写法,无法实现区分标准输出和标准错误,只要换成下面种写法,就可以实现。
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("ls", "-l", "/var/log/*.log")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout // 标准输出
cmd.Stderr = &stderr // 标准错误
err := cmd.Run()
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)
if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err)
}
}
输出如下,可以看到前面的报错内容被归入到标准错误里
$ go run demo.go
out:
err:
ls: cannot access /var/log/*.log: No such file or directory
2020/11/11 19:59:31 cmd.Run() failed with exit status 2
exit status 1
第四种:多条命令组合,请使用管道
将上一条命令的执行输出结果,做为下一条命令的参数。在 Shell 中可以使用管道符 | 来实现。
比如下面这条命令,统计了 message 日志中 ERROR 日志的数量。
$ grep ERROR /var/log/messages | wc -l
19
类似的,在 Golang 中也有类似的实现。
package main
import (
"os"
"os/exec"
)
func main() {
c1 := exec.Command("grep", "ERROR", "/var/log/messages")
c2 := exec.Command("wc", "-l")
c2.Stdin, _ = c1.StdoutPipe()
c2.Stdout = os.Stdout
_ = c2.Start()
_ = c1.Run()
_ = c2.Wait()
}
输出如下
$ go run demo.go
19
第五种:设置命令级别的环境变量
使用 os 库的 Setenv 函数来设置的环境变量,是作用于整个进程的生命周期的。
package main
import (
"fmt"
"log"
"os"
"os/exec"
)
func main() {
os.Setenv("NAME", "wangbm")
cmd := exec.Command("echo", os.ExpandEnv("$NAME"))
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("%s", out)
}
只要在这个进程里,NAME 这个变量的值都会是 wangbm,无论你执行多少次命令
$ go run demo.go
wangbm
如果想把环境变量的作用范围再缩小到命令级别,也是有办法的。
为了方便验证,我新建个 sh 脚本,内容如下
$ cat /home/wangbm/demo.sh
echo $NAME
$ bash /home/wangbm/demo.sh # 由于全局环境变量中没有 NAME,所以无输出
另外,demo.go 里的代码如下
package main
import (
"fmt"
"os"
"os/exec"
)
func ChangeYourCmdEnvironment(cmd * exec.Cmd) error {
env := os.Environ()
cmdEnv := []string{}
for _, e := range env {
cmdEnv = append(cmdEnv, e)
}
cmdEnv = append(cmdEnv, "NAME=wangbm")
cmd.Env = cmdEnv
return nil
}
func main() {
cmd1 := exec.Command("bash", "/home/wangbm/demo.sh")
ChangeYourCmdEnvironment(cmd1) // 添加环境变量到 cmd1 命令: NAME=wangbm
out1, _ := cmd1.CombinedOutput()
fmt.Printf("output: %s", out1)
cmd2 := exec.Command("bash", "/home/wangbm/demo.sh")
out2, _ := cmd2.CombinedOutput()
fmt.Printf("output: %s", out2)
}
执行后,可以看到第二次执行的命令,是没有输出 NAME 的变量值。
$ go run demo.go
output: wangbm
output:
3 命令行参数的解析:flag 库详解
在 Golang 程序中有很多种方法来处理命令行参数。
简单的情况下可以不使用任何库,直接使用 os.Args
package main
import (
"fmt"
"os"
)
func main() {
//os.Args是一个[]string
if len(os.Args) > 0 {
for index, arg := range os.Args {
fmt.Printf("args[%d]=%v\n", index, arg)
}
}
}
试着运行一下,第一个参数是执行文件的路径。
$ go run demo.go hello world hello golang
args[0]=/var/folders/72/lkr7ltfd27lcf36d75jdyjr40000gp/T/go-build187785213/b001/exe/demo
args[1]=hello
args[2]=world
args[3]=hello
args[4]=golang
从上面你可以看到,os.Args 只能处理简单的参数,而且对于参数的位置有严格的要求。对于一些比较复杂的场景,就需要你自己定义解析规则,非常麻烦。
如果真的遇上了所谓的复杂场景,那么还可以使用 Golang 的标准库 flag 包来处理命令行参数。
本文将介绍 Golang 标准库中 flag 包的用法。
3.1 参数种类
根据参数是否为布尔型,可以分为两种:
- 布尔型参数:如 --debug,后面不用再接具体的值,指定就为 True,不指定就为 False非布尔型参数
- 非布尔型参数:非布尔型,有可能是int,string 等其他类型,如 --name jack ,后面可以接具体的参数值
根据参数名的长短,还可以分为:
- 长参数:比如 --name jack 就是一个长参数,参数名前有两个 -
- 短参数:通常为一个或两个字母(是对应长参数的简写),比如 -n ,参数名前只有一个 -
3.2 入门示例
我先用一个字符串类型的参数的示例,抛砖引玉
package main
import (
"flag"
"fmt"
)
func main(){
var name string
flag.StringVar(&name, "name", "jack", "your name")
flag.Parse() // 解析参数
fmt.Println(name)
}
flag.StringVar 定义了一个字符串参数,它接收几个参数
- 第一个参数 :接收值后,存放在哪个变量里,需为指针
- 第二个参数 :在命令行中使用的参数名,比如 --name jack 里的 name
- 第三个参数 :若命令行中未指定该参数值,那么默认值为 jack
- 第四个参数:记录这个参数的用途或意义
运行以上程序,输出如下
$ go run demo.go
jack
$ go run demo.go --name wangbm
wangbm
3.3 改进一下
如果你的程序只接收很少的几个参数时,上面那样写也没有什么问题。
但一旦参数数量多了以后,一大堆参数解析的代码堆积在 main 函数里,影响代码的可读性、美观性。
建议将参数解析的代码放入 init 函数中,init 函数会先于 main 函数执行。
package main
import (
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "jack", "your name")
}
func main(){
flag.Parse()
fmt.Println(name)
}
3.4 参数类型
当你在命令行中指定了参数,Go 如何解析这个参数,转化成何种类型,是需要你事先定义的。
不同的参数,对应着 flag 中不同的方法。
下面分别讲讲不同的参数类型,都该如何定义。
布尔型
实现效果:当不指定 --debug 时,debug 的默认值为 false,你一指定 --debug,debug 为赋值为 true。
var debug bool
func init() {
flag.BoolVar(&debug, "debug", false, "是否开启 DEBUG 模式")
}
func main(){
flag.Parse()
fmt.Println(debug)
}
运行后,执行结果如下
$ go run main.go
false
$ go run main.go --debug
true
数值型
定义一个 age 参数,不指定默认为 18
var age int
func init() {
flag.IntVar(&age, "age", 18, "你的年龄")
}
func main(){
flag.Parse()
fmt.Println(age)
}
运行后,执行结果如下
$ go run main.go
18
$ go run main.go --age 20
20
int64、 uint 和 float64 类型分别对应 Int64Var 、 UintVar、Float64Var 方法,也是同理,不再赘述。
字符串
定义一个 name参数,不指定默认为 jack
var name string
func init() {
flag.StringVar(&name, "name", "jack", "你的名字")
}
func main(){
flag.Parse()
fmt.Println(name)
}
运行后,执行结果如下
$ go run main.go
jack
$ go run main.go --name wangbm
wangbm
时间类型
定义一个 interval 参数,不指定默认为 1s
var interval time.Duration
func init() {
flag.DurationVar(&interval, "interval", 1 * time.Second, "循环间隔")
}
func main(){
flag.Parse()
fmt.Println(interval)
}
验证效果如下
$ go run main.go
1s
$ go run main.go --interval 2s
2s
3.5 自定义类型
flag 包支持的类型有 Bool、Duration、Float64、Int、Int64、String、Uint、Uint64。
这些类型的参数被封装到其对应的后端类型中,比如 Int 类型的参数被封装为 intValue,String 类型的参数被封装为 stringValue。
这些后端的类型都实现了 flag.Value 接口,因此可以把一个命令行参数抽象为一个 Flag 类型的实例。下面是 Value 接口和 Flag 类型的代码:
type Value interface {
String() string
Set(string) error
}
// Flag 类型
type Flag struct {
Name string // name as it appears on command line
Usage string // help message
Value Value // value as set 是个 interface,因此可以是不同类型的实例。
DefValue string // default value (as text); for usage message
}
func Var(value Value, name string, usage string) {
CommandLine.Var(value, name, usage)
}
想要实现自定义类型的参数,其实只要 Var 函数的第一个参数对象实现 flag.Value接口即可
type sliceValue []string
func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
}
func (s *sliceValue) Set(val string) error {
// 如何解析参数值
*s = sliceValue(strings.Split(val, ","))
return nil
}
func (s *sliceValue) String() string {
return strings.Join([]string(*s), ",")
}
比如我想实现如下效果,传入的参数是一个字符串,以逗号分隔,flag 的解析时将其转成 slice。
$ go run demo.go -members "Jack,Tom"
[Jack Tom]
那我可以这样子编写代码
var members []string
type sliceValue []string
func newSliceValue(vals []string, p *[]string) *sliceValue {
*p = vals
return (*sliceValue)(p)
}
func (s *sliceValue) Set(val string) error {
// 如何解析参数值
*s = sliceValue(strings.Split(val, ","))
return nil
}
func (s *sliceValue) String() string {
return strings.Join([]string(*s), ",")
}
func init() {
flag.Var(newSliceValue([]string{}, &members), "members", "会员列表")
}
func main(){
flag.Parse()
fmt.Println(members)
}
有的朋友 可能会对 (*sliceValue)(p) 这行代码有所疑问,这是什么意思呢?
关于这个,其实之前在 【2.9 详细图解:静态类型与动态类型】有讲过,忘记了可以前往复习。
3.6 长短选项
flag 包,在使用上,其实并没有没有长短选项之别,你可以看下面这个例子
package main
import (
"flag"
"fmt"
)
var name string
func init() {
flag.StringVar(&name, "name", "明哥", "你的名字")
}
func main(){
flag.Parse()
fmt.Println(name)
}
通过指定如下几种参数形式
$ go run main.go
明哥
$ go run main.go --name jack
jack
$ go run main.go -name jack
jack
一个 - 和两个 - 执行结果是相同的。
那么再加一个呢?
终于报错了。说明最多只能指定两个 -
$ go run main.go ---name jack
bad flag syntax: ---name
Usage of /tmp/go-build245956022/b001/exe/main:
-name string
你的名字 (default "明哥")
exit status 2
3.7 总结一下
flag 在绝大多数场景下,它是够用的,但如果要支持更多的命令传入格式,flag 可能并不是最好的选择。
那些在标准库不能解决的场景,往往会有相应的Go爱好者提供第三方解决方案。我所了解到的 cobra 就是一个非常不错的库。
它能够支持 flag 不能支持的功能,比如 支持短选项,支持子命令 等等,后面找个机会再好好写一下。
3.8 flag 的函数
Lookup
从众多数参数中查取出 members 的参数值
m := flag.Lookup("members")
4 总结 Go 读文件的 10 种方法
Go 中对文件内容读写的方法,非常地多,其中大多数是基于 syscall 或者 os 库的高级封装,不同的库,适用的场景又不太一样,为免新手在这块上裁跟头,我花了点时间把这些内容梳理了下。
这篇是上篇,先介绍读取文件的 10 种方法,过两天再介绍写文件的。
4.1 整个文件读取入内存
直接将数据直接读取入内存,是效率最高的一种方式,但此种方式,仅适用于小文件,对于大文件,则不适合,因为比较浪费内存。
4.1.1 直接指定文件名读取
有两种方法
第一种:使用 os.ReadFile
package main
import (
"fmt"
"os"
)
func main() {
content, err := os.ReadFile("a.txt")
if err != nil {
panic(err)
}
fmt.Println(string(content))
}
第二种:使用 ioutil.ReadFile
package main
import (
"io/ioutil"
"fmt"
)
func main() {
content, err := ioutil.ReadFile("a.txt")
if err != nil {
panic(err)
}
fmt.Println(string(content))
}
其实在 Go 1.16 开始,ioutil.ReadFile 就等价于 os.ReadFile,二者是完全一致的
// ReadFile reads the file named by filename and returns the contents.
// A successful call returns err == nil, not err == EOF. Because ReadFile
// reads the whole file, it does not treat an EOF from Read as an error
// to be reported.
//
// As of Go 1.16, this function simply calls os.ReadFile.
func ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
4.1.2 先创建句柄再读取
如果仅是读取,可以使用高级函数 os.Open
package main
import (
"os"
"io/ioutil"
"fmt"
)
func main() {
file, err := os.Open("a.txt")
if err != nil {
panic(err)
}
defer file.Close()
content, err := ioutil.ReadAll(file)
fmt.Println(string(content))
}
之所以说它是高级函数,是因为它是只读模式的 os.OpenFile
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
因此,你也可以直接使用 os.OpenFile,只是要多加两个参数
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
file, err := os.OpenFile("a.txt", os.O_RDONLY, 0)
if err != nil {
panic(err)
}
defer file.Close()
content, err := ioutil.ReadAll(file)
fmt.Println(string(content))
}
4.2 每次只读取一行
一次性读取所有的数据,太耗费内存,因此可以指定每次只读取一行数据。方法有三种:
- bufio.ReadLine()
- bufio.ReadBytes(‘:raw-latex:
\n
’) - bufio.ReadString(‘:raw-latex:
\n
’)
在 bufio 的源码注释中,曾说道 bufio.ReadLine() 是低级库,不太适合普通用户使用,更推荐用户使用 bufio.ReadBytes 和 bufio.ReadString 去读取单行数据。
因此,这里不再介绍 bufio.ReadLine()
4.2.1 使用 bufio.ReadBytes
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
// 创建句柄
fi, err := os.Open("christmas_apple.py")
if err != nil {
panic(err)
}
// 创建 Reader
r := bufio.NewReader(fi)
for {
lineBytes, err := r.ReadBytes('\n')
line := strings.TrimSpace(string(lineBytes))
if err != nil && err != io.EOF {
panic(err)
}
if err == io.EOF {
break
}
fmt.Println(line)
}
}
4.2.2 使用 bufio.ReadString
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
// 创建句柄
fi, err := os.Open("a.txt")
if err != nil {
panic(err)
}
// 创建 Reader
r := bufio.NewReader(fi)
for {
line, err := r.ReadString('\n')
line = strings.TrimSpace(line)
if err != nil && err != io.EOF {
panic(err)
}
if err == io.EOF {
break
}
fmt.Println(line)
}
}
4.3 每次只读取固定字节数
每次仅读取一行数据,可以解决内存占用过大的问题,但要注意的是,并不是所有的文件都有换行符 \n。
因此对于一些不换行的大文件来说,还得再想想其他办法。
4.3.1 使用 os 库
通用的做法是:
- 先创建一个文件句柄,可以使用 os.Open 或者 os.OpenFile
- 然后 bufio.NewReader 创建一个 Reader
- 然后在 for 循环里调用 Reader 的 Read 函数,每次仅读取固定字节数量的数据。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 创建句柄
fi, err := os.Open("a.txt")
if err != nil {
panic(err)
}
// 创建 Reader
r := bufio.NewReader(fi)
// 每次读取 1024 个字节
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err != nil && err != io.EOF {
panic(err)
}
if n == 0 {
break
}
fmt.Println(string(buf[:n]))
}
}
4.3.2 使用 syscall 库
os 库本质上也是调用 syscall 库,但由于 syscall 过于底层,如非特殊需要,一般不会使用 syscall
本篇为了内容的完整度,这里也使用 syscall 来举个例子。
本例中,会每次读取 100 字节的数据,并发送到通道中,由另外一个协程进行读取并打印出来。
package main
import (
"fmt"
"sync"
"syscall"
)
func main() {
fd, err := syscall.Open("christmas_apple.py", syscall.O_RDONLY, 0)
if err != nil {
fmt.Println("Failed on open: ", err)
}
defer syscall.Close(fd)
var wg sync.WaitGroup
wg.Add(2)
dataChan := make(chan []byte)
go func() {
defer wg.Done()
for {
data := make([]byte, 100)
n, _ := syscall.Read(fd, data)
if n == 0 {
break
}
dataChan <- data
}
close(dataChan)
}()
go func() {
defer wg.Done()
for {
select {
case data, ok := <-dataChan:
if !ok {
return
}
fmt.Printf(string(data))
default:
}
}
}()
wg.Wait()
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?