1. 程式人生 > >Go語言7

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("檔案拷貝完成")
}

命令列引數

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>
*/

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,增加一下功能:

  • 增加持久化儲存的功能
  • 增加日誌記錄的功能