第五章:学标准库

1 fmt.Printf 方法速查指南

1.1 fmt 的三大函数对比

fmt 标准库是我们在学习和编写 Go 代码,使用最频繁的库之一。
在新手阶段,通常会使用 fmt 包的 打印函数来查看变量的信息。
这样的打印函数,有三个

  1. fmt.Print:正常打印字符串和变量,不会进行格式化,不会自动换行,需要手动添加 \n 进行换行,多个变量值之间不会添加空格
  2. fmt.Println:正常打印字符串和变量,不会进行格式化,多个变量值之间会添加空格,并且在每个变量值后面会进行自动换行
  3. 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 对象,根据不同的需求,可以将命令的执行分为三种情况

  1. 只执行命令,不获取结果
  2. 执行命令,并获取结果(不区分 stdout 和 stderr)
  3. 执行命令,并获取结果(区分 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 种方法,过两天再介绍写文件的。
image.png

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 每次只读取一行

一次性读取所有的数据,太耗费内存,因此可以指定每次只读取一行数据。方法有三种:

  1. bufio.ReadLine()
  2. bufio.ReadBytes(‘:raw-latex:\n’)
  3. 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()
}
posted @ 2024-03-14 23:21  liuyang9643  阅读(5)  评论(0编辑  收藏  举报