Go语言7

终端读写

操作终端相关文件句柄常量:

  • os.Stdin : 标准输入
  • os.Stdout : 标准输出
  • os.Stderr : 标准错误

这个是fmt包里的一个方法,打印到文件。比平时用的fmt打印多一个参数,这个参数接收的就是文件句柄,一个实现了 io.Winter 的接口:

func Fprint(w io.Writer, a ...interface{}) (n int, err error)

把终端的标准输出的文件句柄传入,就是打印到标准输出,即屏幕:

package main

import (
    "os"
    "fmt"
)

func main(){
    fmt.Fprintln(os.Stdout, "TEST")
}

终端输入

先打印提示信息,然后获取用户输入的值,最后打印出来:

package main

import "fmt"

var firstName, lastName string

func main(){
    fmt.Print("Please enter your full name:")
    fmt.Scanln(&firstName, &lastName)
    // fmt.Scanf("%s %s", &firstName, &lastName)  // 和上面那句效果一样
    fmt.Printf("Hi %s %s.\n", firstName, lastName)
}

把字符串作为格式的化输入

使用 fmt 包里的 Sscanf()方法:

func Sscanf(str string, format string, a ...interface{}) (n int, err error)

Scanf 扫描实参 string,并将连续由空格分隔的值存储为连续的实参, 其格式由 format 决定。它返回成功解析的条目数。
不是很好理解的话,参考下下面的例子:

package main

import "fmt"

func main(){
    var (
        input = "12.34 567 Golang"  // 要扫描的字符串
        format = "%f %d %s"  // 每段字符串的格式
        i float32  // 对应格式的变量,把字符串里的每一段赋值到这些变量里
        j int
        k string
    )
    fmt.Sscanf(input, format, &i, &j, &k)
    fmt.Println(i)
    fmt.Println(j)
    fmt.Println(k)
}

带缓冲区的读写

不直接操作 io,在缓冲区里进行读写,io的操作交由操作系统处理,主要是解决性能的问题。
这里要使用 bufio 包,下面是缓冲区进行读操作的示例:

package main

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

func main() {
    var inputReader *bufio.Reader  // bufio包里的一个结构体类型
    // 给上面的结构体赋值,包里提供了构造函数
    inputReader = bufio.NewReader(os.Stdin)  // 生成实例,之后要调用里面的方法
    fmt.Print("请随意输入内容: ")
    // 调用实例的方法进行读操作,就是带缓冲区的操作了
    input, err := inputReader.ReadString('\n')  // 这里是字符类型
    if err == nil {
        fmt.Println(input)
    }
}

上面是从终端读取,文件读写下面会讲,先来个从文件读取的示例:

package main

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

func main() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("ERROR:", err)
        return
    }
    defer file.Close()  // 函数返回时关闭文件
    bufReader := bufio.NewReader(file)
    for {
        line, err := bufReader.ReadString('\n')
        // 最后一行会同时返回 line 和 err,所以先打印
        fmt.Println(strings.TrimSpace(line))
        if err != nil {
            if err == io.EOF {
                fmt.Println("读取完毕")
                break
            } else {
                fmt.Println("读取文件错误:", err)
                return
            }
        }
    }
}

这里逐行读取的方法不是太好,下一节的最后有更好的示例。

文件读写

os.File 是个结构体,封装了所有文件相关的操作。之前讲的 os.Stdin、os.Stdout、os.Stderr 都是文件句柄,都是 *os.File

读取整个文件

"io/ioutil" 可以直接把整个文件读取出来,适合文件不是很大的情况:

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    buf, err := ioutil.ReadFile("test.txt")
    if err != nil {
        fmt.Println("ERROR", err)
        return
    }
    fmt.Println(string(buf))  // buf是[]byte类型,要转字符串
    // 写操作
    err = ioutil.WriteFile("wtest.txt", buf, 0x64)
    if err != nil {
        panic(err.Error())
    }
}

上面还有整个文件写入的操作。

读取压缩文件

下面的代码是解压读取一个 .gz 文件,注意不是 .tar.gz 。打了tar包应该是不行的:

package main

import (
    "compress/gzip"
    "os"
    "fmt"
    "bufio"
    "io"
    "strings"
)

func main() {
    fileName := "test.gz"
    var reader *bufio.Reader
    file, err := os.Open(fileName)
    if err != nil {
        fmt.Println("Open ERROE:", err)
        os.Exit(1)
    }
    defer file.Close()  // 记得关文件
    gzFile, err := gzip.NewReader(file)
    if err != nil {
        fmt.Println("gz ERROR:", err)
        return
    }
    reader = bufio.NewReader(gzFile)
    for {
        line, err := reader.ReadString('\n')
        fmt.Println(strings.TrimSpace(line))
        if err != nil {
            if err == io.EOF {
                fmt.Println("读取完毕")
                break
            } else {
                fmt.Println("Read ERROR:", err)
                os.Exit(0)
            }
        }
    }
}

文件写入

写入文件的命令:

os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)

第二个参数是文件打开模式:

  • os.O_WRONLY : 只写
  • os.O_CREATE : 创建文件
  • os.O_RDONLY : 只读
  • os.O_RDWR : 读写
  • os.O_TRUNC : 清空

第三个参数是权限控制,同Linux的ugo权限。
文件写入的示例:

package main

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

func main() {
    outputFile, err := os.OpenFile("test.txt", os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println("ERROR", err)
        return
    }
    defer outputFile.Close()
    outputWriter := bufio.NewWriter(outputFile)
    outputString := "Hello World! "
    for i := 0; i < 10; i++ {
        outputWriter.WriteString(outputString + strconv.Itoa(i) + "\n")
    }
    outputWriter.Flush()  // 强制刷新,保存到磁盘
}

拷贝文件

首先分别打开2个文件,然后拷贝文件只要一次调用传入2个文件句柄就完成了:

package main

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

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        fmt.Println("Open ERROR", err)
        return
    }
    defer src.Close()
    dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
        fmt.Println("OpenFile ERROR", err)
        return
    }
    defer dst.Close()
    // 先依次把2个文件都打开,然后拷贝只要下面这一句
    return io.Copy(dst, src)
}

func main() {
    CopyFile("test_copy.txt", "test.txt")
    fmt.Println("文件拷贝完成")
}

逐行读取

这个是《Go程序设计语言》里的一个例子,代码在这里也有:
https://github.com/adonovan/gopl.io/tree/master/ch1/dup2
上面已经有逐行读取的例子,但是这个例子的使用的方法应该更好。首先创建 bufio.Scanner 类型,然后调用 Scan() 方法,每一次调用就是读取下一行,并且会将结尾的换行符去掉。最后通过 Text() 方法来获取读到的内容。并且 Scan() 方法能读到新的一行是返回 true ,如果读不到内容了就会返回 false 。

// 打印输入中多次出现的行的个数和文本
// 它从 stdin 或指定的文件列表读取
package main

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

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

func countLines(f *os.File, counts map[string]int) {
    input := bufio.NewScanner(f)
    for input.Scan() {
        counts[input.Text()]++
    }
    // 注意: 忽略了 input.Err() 中有可能出现的错误
}

另外,这段程序的功能是找出重复的行并统计重复了几次,这里实现的方法也值得参考。简单来说就是使用map,把内容作为key,默认的value就是重复的次数,初始值是0。每次都是往map里追加一行的内容,如果没有这个key就是生成一个key-value,如果有相同的key,就是该行重复了,则把value自增1。
这里的错误处理,使用了简单的错误处理方法。使用Fprintf 和 %v 。这样就把信息从标准错误流上输出了。

命令行参数

os.Args 是一个 string 的切片,用来存储所有的命令行参数。

package main

import (
    "os"
    "fmt"
)

func main() {
    fmt.Println(len(os.Args))
    for i, v := range os.Args {
        fmt.Println(i, v)
    }
}

/* 执行结果
PS H:\Go\src\go_dev\day7\args\beginning> go run main.go arg1 arg2 arg3
4
0 [省略敏感信息]\main.exe
1 arg1
2 arg2
3 arg3
PS H:\Go\src\go_dev\day7\args\beginning>
*/

os.Args 至少有一个元素,如果一个参数也不打,第一个元素就是命令本身。之后的命令行参数从下标1开始存储。

解析命令行参数

flag 包实现命令行标签解析。

func BoolVar(p *bool, name string, value bool, usage string)
func StringVar(p *string, name string, value string, usage string)
func IntVar(p *int, name string, value int, usage string)

第一个参数是个指针,指向要接收的参数的值
第二个参数是指定的名字
第三个参数是默认值
第四个参数是用法说明
用法示例:

package main

import (
    "fmt"
    "flag"
)

func main() {
    var (
        enable bool
        conf string
        num int
    )
    flag.BoolVar(&enable, "b", false, "是否启用")
    flag.StringVar(&conf, "s", "test.conf", "配置文件")
    flag.IntVar(&num, "i", 0, "数量")
    flag.Parse()  // 读取命令行参数进行解析
    fmt.Println(enable, conf, num)
}

/* 执行结果
PS H:\Go\src\go_dev\day7\args\flag_var> go run main.go
false test.conf 0
PS H:\Go\src\go_dev\day7\args\flag_var> go run main.go -b -s default.conf -i 10
true default.conf 10
PS H:\Go\src\go_dev\day7\args\flag_var>
*/

自定义类型标签

使用自定义类型的标签,变量需要满足flag.Value接口。接口如下:

type Value interface {
    String() string
    Set(string) error
}

这里以字符串切片为例,先定义一个别名类型,然后实现上面的接口:

type sliceValue []string // 字符串切片的别名类型

// 实现接口的String方法
func (s *sliceValue) String() string {
    return fmt.Sprint(*s) // 返回值是用于诊断的,所以对正常使用没什么影响
}

// 实现接口的Set方法
func (s *sliceValue) Set(value string) error {
    if len(*s) > 0 {
        return errors.New("sliceValue flag already set")
    }
    for _, str := range strings.Split(value, ",") {
        *s = append(*s, str)
    }
    return nil
}

上面的Set方法里是通过逗号(,)进行字符串的分隔从而获得一个字符串切片。
另外Set开头还有一个if判断,检查该变量被重复设置,类型下面这样的调用就属于重复设置,会返回错误:

go run main.go -s v1 -s v2

不过去掉这个if判断,能实现更多的效果,后面会讲。
最后来解析使用这个自定义的命令行参数获取字符串切片:

func main() {
    var sliceFlag sliceValue  // 要获取命令行参数的变量
    flag.Var(&sliceFlag, "s", "flag Var")  // 获取命令行参数
    flag.Parse()  // 解析
    fmt.Printf("%T\n", &sliceFlag)  // 类型还是自定义的别名类型
    s := []string(sliceFlag)  // 类型转换
    fmt.Printf("% q\n", s)
}

/* 执行结果
PS H:\Go\src\localdemo\flag_demo> go run main.go -s v1,v2,v3
*main.sliceValue
["v1" "v2" "v3"]
PS H:\Go\src\localdemo\flag_demo>
*/

重复调用-s参数来获得切片
其实上面获取切片的方法有局限性,比如遇到字符串需要包含逗号(,)的情况。而且这样的实现方法也可以不需要通过自定义命令行参数来实现,而是直接获取到命令行的字符串类型,然后再做Split就好了。
改写Set方法,必须去掉开头的重复设置的检查,具体如下:

func (s *sliceValue) Set(value string) error {
    *s = append(*s, value)
    return nil
}

现在只要用下面的方法来调用就可以把每次调用的参数是的字符串添加到字符串切片里了:

PS H:\Go\src\localdemo\flag_demo> go run main.go -s v1 -s v2 -s v3
*main.sliceValue
["v1" "v2" "v3"]
PS H:\Go\src\localdemo\flag_demo>

关于Set方法
从上面的实现效果可以看出,Set方法是在每次遇到-s参数的时候都会调用一次,并把参数后面的字符串传值给Set方法的value参数。

关于String方法,设置默认值
自定义类型标签的默认值,就是该类型的初始值。
但是也是有方法来设置默认值的。首先,关于String方法,下面这句英文怎么理解:

String is the method to format the flag's value

该方法的返回值在这里不重要,但是该方法会在开始的时候调用一次,而且里面有变量的指针,所以可以在里面改变变量的值。所以改写String方法如下:

func (s *sliceValue) String() string {
    *s = []string{"default"}
    return fmt.Sprint(*s) // 返回值是用于诊断的,所以对正常使用没什么影响
}

现在变量就会有一个初始值了。如果Set方法是改变变量的值,就没有问题。但如果Set方法里是像例子中那样的添加元素的方法,就不会把这个初始值覆盖掉:

PS H:\Go\src\localdemo\flag_demo> go run main.go
*main.sliceValue
["default"]
PS H:\Go\src\localdemo\flag_demo> go run main.go -s v1
*main.sliceValue
["default" "v1"]

第一次调用,有默认的初始值,第二次调用的时候,调用了参数,但是只是添加了元素,没有把之前的初始值覆盖掉。
通过修改Set方法,只能解决部分逻辑上的问题:

func (s *sliceValue) Set(value string) error {
    if len(*s) == 1 && (*s)[0] == "default" {
        *s = nil
    }
    *s = append(*s, value)
    return nil
}

但是其实还是不能彻底解决所有的逻辑问题:

PS H:\Go\src\localdemo\flag_demo> go run main.go -s v1
*main.sliceValue
["v1"]
PS H:\Go\src\localdemo\flag_demo> go run main.go -s default -s default
*main.sliceValue
["default"]
PS H:\Go\src\localdemo\flag_demo>

要彻底解决应该需要一个全局变量。

Json数据协议

导入包

import "encoding/json"

序列化

json.Marshal(data interface{})

示例:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    UserName string  `json:"username"`
    NickName string  `json:"nickname"`
    Age int  `json:"age"`
    Vip bool  `json:"vip"`
}

func main() {
    u1 := &User{
        UserName: "Sara",
        NickName: "White Canary",
        Age: 29,
        Vip: true,
    }
    if data, err := json.Marshal(u1); err == nil{
        fmt.Println(string(data))
    }
}

反序列化

json.Unmarshal(data []byte, v interface{})

示例:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    UserName string  `json:"username"`
    NickName string  `json:"nickname"`
    Age int  `json:"age"`
    Vip bool  `json:"vip"`
}

func main() {
    var jsonStr = `{
        "username": "Kara",
        "nickname": "Supergirl",
        "age": 20,
        "vip": false
        }`
    var jsonByte = []byte(jsonStr)
    var u2 User
    if err := json.Unmarshal(jsonByte, &u2); err == nil {
        fmt.Println(u2)
    } else {
        fmt.Println("ERROR:", err)
    }
}

错误处理

error 类型是在 builtin 包里定义的。error 是个接口,里面实现了一个 Error() 的方法,返回一个字符串:

type error interface {
    Error() string
}

所以其实 error 也就是个字符串信息。

定义错误

error 包实现了用于错误处理的函数。
New 返回一个按给定文本格式化的错误:

package main

import (
    "errors"
    "fmt"
)

var errNotFound error = errors.New("Not found error")

func main() {
    fmt.Println("ERROR:", errNotFound)
}

平时简单这样用用就可以了,也很方便。不过学习嘛,下面稍微再深入点。

自定义错误

主要是学习,上面的 New() 函数用起来更加方便。
使用自定义错误返回:

package main

import (
    "fmt"
    "os"
)

type PathError struct {
    Op string
    Path string
    err string  // 把这个信息隐藏起来,所以是小写
}

// 实现error的接口
func (e *PathError) Error() string {
    return e.Op + " " + e.Path + " 路径不存在\n原始错误信息: " + e.err
}

func Open(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return &PathError{
            Op: "read",
            Path: filename,
            err: err.Error(),
        }
    }
    defer file.Close()
    return nil
}

func main() {
    err := Open("test.txt")
    if err != nil {
        fmt.Println(err)
    }
}

/* 执行结果
PS H:\Go\src\go_dev\day7\error\diy_error> go run main.go
read test.txt 路径不存在
原始错误信息: open test.txt: The system cannot find the file specified.
PS H:\Go\src\go_dev\day7\error\diy_error>
*/

判断自定义错误

这里用 switch 来判断:

switch err := err.(type) {
case ParseError:
    PrintParseError(err)
case.PathError:
    PrintPathError(err)
default:
    fmt.Println(err)
}

异常和捕捉

首先调用 panic 来抛出异常:

package main

func badCall() {
    panic("bad end")
}

func main() {
    badCall()
}

/* 执行结果
PS H:\Go\src\go_dev\day7\error\panic> go run main.go
panic: bad end

goroutine 1 [running]:
main.badCall()
        H:/Go/src/go_dev/day7/error/panic/main.go:4 +0x40
main.main()
        H:/Go/src/go_dev/day7/error/panic/main.go:8 +0x27
exit status 2
PS H:\Go\src\go_dev\day7\error\panic>
*/

执行后就抛出异常了,但是这样程序也崩溃了。
下面来捕获异常,go里没有try之类来捕获异常,所以panic了就是真的异常了,但是还不会马上就崩溃。panic的函数并不会立刻返回,而是先defer,再返回。如果有办法将panic捕获到,并阻止panic传递,就正常处理,如果没有没有捕获,程序直接异常终止。这里并不是像别的语言里那样捕获异常,因为即使捕获到了,也只是执行defer,之后还是要异常终止的,而不是继续在错误的点往下执行。
注意:就像上面说的,在go里panic了就是真的异常了。recover之后,逻辑并不会恢复到panic那个点去,函数还是会在defer之后返回。
下面是使用 defer 处理异常的示例:

package main

import "fmt"

func badCall() {
    panic("bad end")
}

func test() {
    // 用defer在最后捕获异常
    defer func() {
        if e := recover(); e != nil {
            fmt.Println("Panic", e)
        }
    }()
    badCall()
}

func main() {
    test()
}

所以像 python 里的 try except 那样捕获异常,在go里,大概就是返回个 err(error类型) ,然后判断一下 err 是不是 nil。

课后作业

实现一个图书管理系统v3,增加一下功能:

  • 增加持久化存储的功能
  • 增加日志记录的功能

转载于:https://blog.51cto.com/steed/2315597

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值