欢迎访问我的博客,目前从事Machine Learning,欢迎交流

【读书笔记&个人心得】第12章:读写数据

读写数据

除了 fmt 和 os 包,我们还需要用到 bufio 包来处理缓冲的输入和输出。

键盘输入

// 从控制台读取输入:
package main

import "fmt"

var (
	firstName, lastName, s string
	i                      int
	f                      float32
	input                  = "56.12 / 5212 / Go"
	format                 = "%f / %d / %s"
)

func main() {
	fmt.Scanln(&firstName, &lastName)// 空格切换下一个变量接收,回车结束输入
	fmt.Scanf("%s %s", &firstName, &lastName)// 同Scanln,多了按预设格式去读取
	fmt.Sscanf(input, format, &f, &i, &s)// 从字符串读取
}

缓冲读取器 (buffered reader) 来读取数据

package main
import (
    "fmt"
    "bufio"
    "os"
)

var inputReader *bufio.Reader
var input string
var err error

func main() {
    inputReader = bufio.NewReader(os.Stdin) //创建一个读取器,并将其与标准输入绑定
    fmt.Println("Please enter some input: ")
    input, err = inputReader.ReadString('\n')//以\n结束输入,\n也会被读入
    if err == nil {
        fmt.Printf("The input was: %s\n", input)
    }
}

所谓标准输入输出就是 使用命令行(终端) 输入输出

bufio.NewReader() 构造函数的签名为:func NewReader(rd io.Reader) *Reader
该函数的实参可以是满足 io.Reader 接口的任意对象(任意包含有适当的 Read() 方法的对象,请参考章节 11.8),函数返回一个新的带缓冲的 io.Reader 对象,它将从指定读取器(例如 os.Stdin)读取内容。

//TODO 文件结束
ReadString 返回读取到的字符串,如果碰到错误则返回 nil。如果它一直读到文件结束(文件结束是啥意思),则返回读取到的字符串和 io.EOF(一个errors类型)。如果读取过程中没有碰到 delim 字符,将返回错误 err != nil

注意: 在之前的例子中,我们看到,Unix 和 Linux 的行结束符是 \n,而 Windows 的行结束符是 \r\n。在使用 ReadString 和 ReadBytes 方法的时候,我们不需要关心操作系统的类型,直接使用 \n 就可以了。另外,我们也可以使用 ReadLine() 方法来实现相同的功能。

package main
import (
    "fmt"
    "os"
    "bufio"
)

func main() {
    inputReader := bufio.NewReader(os.Stdin)
    fmt.Println("Please enter your name:")
    input, err := inputReader.ReadString('\n')

    if err != nil {
        fmt.Println("There were errors reading, exiting program.")
        return
    }

    fmt.Printf("Your name is %s", input)
    // For Unix: test with delimiter "\n", for Windows: test with "\r\n"
    switch input {
    case "Philip\r\n":  fmt.Println("Welcome Philip!")
    case "Chris\r\n":   fmt.Println("Welcome Chris!")
    case "Ivo\r\n":     fmt.Println("Welcome Ivo!")
    default: fmt.Printf("You are not welcome here! Goodbye!")
    }

    // version 2:   
    switch input {
    case "Philip\r\n":  fallthrough
    case "Ivo\r\n":     fallthrough
    case "Chris\r\n":   fmt.Printf("Welcome %s\n", input)
    default: fmt.Printf("You are not welcome here! Goodbye!\n")
    }

    // version 3:
    switch input {
    case "Philip\r\n", "Ivo\r\n":   fmt.Printf("Welcome %s\n", input)
    default: fmt.Printf("You are not welcome here! Goodbye!\n")
    }
}

根据我的实验,并不能通过输入\n来代替键盘回车

练习

编写一个程序,从键盘读取输入。当用户输入 'S' 的时候表示输入结束,这时程序输出 3 个数字:
i) 输入的字符的个数,包括空格,但不包括 '\r' 和 '\n'
ii) 输入的单词的个数
iii) 输入的行数

// Q28_word_letter_count.go
package main

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

var nrchars, nrwords, nrlines int

func main() {
	nrchars, nrwords, nrlines = 0, 0, 0
	inputReader := bufio.NewReader(os.Stdin)
	fmt.Println("Please enter some input, type S to stop: ")
	for {
		input, err := inputReader.ReadString('\n')
		if err != nil {
			fmt.Printf("An error occurred: %s\n", err)
		}
		if input == "S\r\n" { // Windows, on Linux it is "S\n"
			fmt.Println("Here are the counts:")
			fmt.Printf("Number of characters: %d\n", nrchars)
			fmt.Printf("Number of words: %d\n", nrwords)
			fmt.Printf("Number of lines: %d\n", nrlines)
			os.Exit(0)
		}
		Counters(input)
	}
}

func Counters(input string) {
	nrchars += len(input) - 2 // -2 for \r\n
	nrwords += len(strings.Fields(input))
	nrlines++
}

我的算法

package main

import (
	"bufio"
	"os"
)

func main() {
	inputReader := bufio.NewReader(os.Stdin)
	input, err := inputReader.ReadString('S')

	if err != nil {
		return
	}
	strByte := []byte(input)
	var total int
	for _, v := range strByte {
		var b byte = '\r'
		var b2 byte = '\n'
		if v != b && v != b2 {
			total++
		}
	}

	println(total)
}

读写文件

在 Go 语言中,文件使用 os.File 类型的指针来表示,也叫做文件句柄

读文件的第一行

package main

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

func main() {
    inputFile, inputError := os.Open("input.dat") // Open就是以只读模式打开文件
    if inputError != nil { // 文件不存在或者 没权限打开时报错
        fmt.Printf("An error occurred on opening the inputfile\n" +
            "Does the file exist?\n" +
            "Have you got access to it?\n")
        return // exit the function on error
    }
    defer inputFile.Close() // 函数结束时关闭文件

    inputReader := bufio.NewReader(inputFile)
    for { // 死循环,每次读一行,读到文件尾时readerError 将为非空(io.EOF)
        inputString, readerError := inputReader.ReadString('\n') // 也可以使用 ReadLine(),
        fmt.Printf("The input was: %s", inputString)
        if readerError == io.EOF {
            return
        }      
    }
}

通过使用 bufio 包提供的读取器(写入器也类似),如上面程序所示,我们可以很方便的操作相对高层的 string 对象,而避免了去操作比较底层的字节

将整个文件的内容读到一个字符串里

不用 reader而用ReadFile

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	inputFile := "C:/Users/Administrator/Desktop/abc.txt"
	outputFile := "C:/Users/Administrator/Desktop/abc_copy.txt"
	buf, err := ioutil.ReadFile(inputFile) // os.WriteFile
	if err != nil {
		fmt.Fprintf(os.Stderr, "File Error: %s\n", err) // 把错误信息输出到标准错误流?
		// panic(err.Error())
	}
	fmt.Printf("%s\n", string(buf))
	err = ioutil.WriteFile(outputFile, buf, 0644) // os.WriteFile , 0644 is oct, not hex
	if err != nil {
		panic(err.Error())
	}
}


这0644与unix有关,不是Go的内容,见https://blog.csdn.net/searchwang/article/details/38442041

带缓冲的读取

怎么 读取 不按行划分 文件 或 二进制文件?
在很多情况下,文件的内容是不按行划分的,或者干脆就是一个二进制文件。在这种情况下,ReadString() 就无法使用了,我们可以使用 bufio.Reader 的 Read()

所谓带缓冲的读取,就是设置好一个固定大小的切片作buffer,每次读满buffer然后停止往下读,使用该buffer读到的内容,然后进入下一次循环,继续往下读。

buf := make([]byte, 1024)
...
n, err := inputReader.Read(buf) // 变量 n 的值表示读取到的字节数
if (n == 0) { break}

按列读取 文件中的数据

如果数据是按列排列并且列与列之间用空格分隔的,你可以使用 fmt 包提供的以 FScan... 开头的一系列函数来读取他们。

abc.txt

ABC 40 150
FUNC 56 280
GO 45 356

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Open("C:/Users/Administrator/Desktop/abc.txt")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	var col1, col2, col3 []string
	for {
		var v1, v2, v3 string
		_, err := fmt.Fscanln(file, &v1, &v2, &v3)
		// scans until newline
		if err != nil {
			break
		}
		col1 = append(col1, v1)
		col2 = append(col2, v2)
		col3 = append(col3, v3)
	}

	fmt.Println(col1)
	fmt.Println(col2)
	fmt.Println(col3)
}

输出
[ABC FUNC GO]
[40 56 45]
[150 280 356]

处理文件名和路径

path 包里包含一个子包叫 filepath,这个子包提供了跨平台的函数,用于处理文件名和路径。例如 Base() 函数用于 文件名:

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	var path string = "C:/Users/Administrator/Desktop/abc.txt"
	filename := filepath.Base(path)
	fmt.Printf("%v", filename) // abc.txt
}

解析 CSV 文件

解析 CSV 文件,encoding/csv 包提供了相应的功能。

练习

products.txt

"The ABC of Go";25.5;1500
"Functional Programming with Go";56;280
"Go for It";45.9;356
"The Go Way";55;500

每行的第一个字段为标题,第二个字段为价格,第三个字段为数量。内容的格式基本与 示例 12.3c 的相同,除了分隔符改成了分号。请读取出文件的内容,创建一个结构用于存取一行的数据,然后使用结构的切片,并把数据打印出来。

package main

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

type Product struct {
	name  string
	price float32
	num   int32
}

var Products = make([]Product, 0, 0)

func main() {
	var path string = "C:/Users/Administrator/Desktop/products.txt"
	f, err := os.Open(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "File Error: %s\n", err)
	}
	defer f.Close() // 要在if err != nil之后

	reader := bufio.NewReader(f)

	for {
		line, readerError := reader.ReadString('\n')
		if readerError == io.EOF {
			break // 读取到文件末尾,退出循环
		}

		if len(line) == 0 { // 过滤空行
			continue
		}

		str := string(line[:len(line)-2]) // // remove \r and \n so 2(in Windows, in Linux only \n, so 1)
		strs := strings.Split(str, ";")

		product := new(Product)

		product.name = strs[0]
		price, err := strconv.ParseFloat(strs[1], 32)
		if err != nil {
			fmt.Printf("Error in file: %v", err)
		}
		product.price = float32(price)

		num, err := strconv.Atoi(strs[2])
		if err != nil {
			fmt.Printf("Error in file: %v", err)
		}
		product.num = int32(num)

		Products = append(Products, *product)
	}
	fmt.Println(Products)
}

compress 包:读取压缩文件

compress 包提供了读取压缩文件的功能,支持的压缩文件格式为:bzip2、flate、gzip、lzw 和 zlib。

package main

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

func main() {
	fName := "MyFile.gz"
	var r *bufio.Reader
	fi, err := os.Open(fName)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%v, Can't open %s: error: %s\n", os.Args[0], fName,
			err)
		os.Exit(1)
	}
	defer fi.Close()

    //如果尝试使用压缩格式读取失败就使用普通格式读取
	fz, err := gzip.NewReader(fi)
	if err != nil {
		r = bufio.NewReader(fi)
	} else {
		r = bufio.NewReader(fz)
	}

	for {
		line, err := r.ReadString('\n')
		if err != nil {
			fmt.Println("Done reading file")
			os.Exit(0)
		}
		fmt.Println(line)
	}
}

写文件

package main

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

func main () {
	// var outputWriter *bufio.Writer
	// var outputFile *os.File
	// var outputError os.Error
	// var outputString string
	outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
	if outputError != nil {
		fmt.Printf("An error occurred with file opening or creation\n")
		return  
	}
	defer outputFile.Close()

	outputWriter := bufio.NewWriter(outputFile) // 写入器
	outputString := "hello world!\n"

	for i:=0; i<10; i++ { // 写10次内容到缓冲区
		outputWriter.WriteString(outputString)
	}
	outputWriter.Flush()
}

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

os.O_WRONLY|os.O_CREATE的意思是:我们以只写模式打开文件 output.dat,如果文件不存在则自动创建

可以看到,OpenFile 函数有三个参数:文件名、一个或多个标志(使用 按位或运算符 | 连接),使用的文件权限

os.O_RDONLY:只读
os.O_WRONLY:只写
os.O_CREATE:创建:如果指定文件不存在,就创建该文件。
os.O_TRUNC:截断:如果指定文件已存在,就将该文件的长度截为 0

在读文件的时候,文件的权限是被忽略的,所以在使用 OpenFile() 时传入的第三个参数可以用 0 。而在写文件时,不管是 Unix 还是 Windows,都需要使用 0666。

(我认为设立缓冲区主要是为了节省IO资源,减少IO访问,有IO访问就有等待)
缓冲区的内容紧接着被完全写入文件:outputWriter.Flush()

网上补充:
| 和 || 的区别:
最终得到的boolean值结果一样,都是“或者or”的意思。
| 既是逻辑运算符也是位运算符;|| 只是逻辑运算符。
| 不具有短路效果,即左边true,右边还会执行;|| 具有短路效果,左边为true,右边则不执行。

快速写入

如果写入的东西很简单,我们可以使用 fmt.Fprintf(outputFile, "Some test data.\n") 直接将内容写入文件。fmt 包里的 F... 开头的 Print() 函数可以直接写入任何 io.Writer,包括文件(请参考章节 12.8)。

package main

import "os"

func main() {
	os.Stdout.WriteString("hello, world\n")
	f, _ := os.OpenFile("test", os.O_CREATE|os.O_WRONLY, 0666)
	defer f.Close()
	f.WriteString("hello, world in a file\n")
}

我们以只写模式创建或打开文件 "test" ,并且忽略了可能发生的错误:f, _ := os.OpenFile("test", os.O_CREATE|os.O_WRONLY, 0666)

我们不使用缓冲区,直接将内容写入文件:f.WriteString()

小结

现在可以调 写 的包有
bufio、ioutil、os

写比较注重缓冲和不带缓冲,而读好似不太注重这个,都是一行行地读。

练习

程序中的数据结构如下,是一个包含以下字段的结构:

type Page struct {
Title string
Body []byte
}
请给这个结构编写一个 save() 方法,将 Title 作为文件名、Body 作为文件内容,写入到文本文件中。

再编写一个 load() 函数,接收的参数是字符串 title,该函数读取出与 title 对应的文本文件。请使用 *Page 做为参数,因为这个结构可能相当巨大,我们不想在内存中拷贝它。请使用 ioutil 包里的函数(参考章节 12.2.1)。

package main

import (
	"fmt"
	"io/ioutil"
)

type Page struct {
	Title string
	Body  []byte
}

func (this *Page) save() (err error) {
	return ioutil.WriteFile(this.Title, this.Body, 0666) // 若文件存在则覆盖,若文件不存在则创建
}

func (this *Page) load(title string) (err error) {
	this.Title = title
	this.Body, err = ioutil.ReadFile(this.Title)
	return err
}

func main() {
	page := Page{
		"C:/Users/Administrator/Desktop/Page.md",
		[]byte("# Page\n## Section1\nThis is section1."),
	}
	page.save()

	// load from Page.md
	var new_page Page
	new_page.load("C:/Users/Administrator/Desktop/Page.md")
	fmt.Println(string(new_page.Body))
}

文件拷贝

使用os+io包

// filecopy.go
package main

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

func main() {
	CopyFile("target.txt", "source.txt")
	fmt.Println("Copy done!")
}

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close()

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

注意 defer 的使用:当打开 dst 文件时发生了错误,那么 defer 仍然能够确保 src.Close() 执行。(两个defer互不影响)

os的包基本都有能力在找不到打开文件时创建一个

从命令行读取参数

os 包

os 包中有一个 string 类型的切片变量 os.Args,它在程序启动后读取命令行输入的参数
os.Args[0] 放的是程序本身的名字,在本例中是 os_args

// os_args.go
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	who := "Alice "
	if len(os.Args) > 1 {
		who += strings.Join(os.Args[1:], " ") // 命令行参数会放置在切片 os.Args[] 中(以空格分隔)
	}
	fmt.Println("Good Morning", who)
}

flag 包

定义 命令行参数 或 称命令行标志
flag 包有一个扩展功能用来解析命令行选项。但是通常被用来替换基本常量
https://blog.csdn.net/cleanairdoris/article/details/84455552
https://cloud.tencent.com/developer/section/1141707

在 flag 包中有一个 Flag

type Flag struct {
	Name     string // name as it appears on command line
	Usage    string // help message
	Value    Value  // value as set
	DefValue string // default value (as text); for usage message
}

下面模拟echo 的功能,其实就是对命令行的一些 关键字 代表的 行为 进行解析,用法为一个flag + 一个操作对象,不支持多flag

package main

import (
	"flag" // command line option parser
	"os"
)

var NewLine = flag.Bool("n", false, "print newline") // 在echo使用-n来使用这个flag,这里默认是关闭的,用布尔值false表示

const (
	Space   = " "
	Newline = "\n"
)

func main() {
	flag.PrintDefaults() //打印 flag 的使用帮助信息
	flag.Parse()         // 解析:扫描参数列表(或者常量列表)并设置 flag(应该是根据命令行参数设置对应得flag为true)
	var s string = ""
	for i := 0; i < flag.NArg(); i++ { //flag.Narg() 返回参数的数量,不包括-n这些
		// fmt.Println(*NewLine)
		if i > 0 { // 有参数时,flag才有效,才有作用的对象
			//如果参数是-n
			if *NewLine { // 当命令行参数有-n且执行了Parse()后,-n关联的flag会变为true
				s += Newline
			}
		}

		//普通参数,直接输出
		//Parse() 之后 flag.Arg(i) 全部可用
		//flag.Arg(i) 表示第 i 个参数
		//flag.Arg(0) 就是第一个真实的 flag,而不是像 os.Args(0) 放置程序的名字
		s += flag.Arg(i)
		s += " "
	}
	os.Stdout.WriteString(s)
}

结果
-n print newline
abc
efg
hij

(前面的flag(-n)对后面的三个参数都有效)

flag.VisitAll(fn func(*Flag)) 是另一个有用的功能:按照字典顺序遍历 flag,并且对每个标签调用 fn (参考 15.8 章的例子)
要给 flag 定义其它类型,可以使用 flag.Int(),flag.Float64(),flag.String()。
在第 15.8 章你将找到一个具体的例子。

用切片读写文件

本程序如果不指定文件名,那么就是使用标准流文件,在命令行输入内容后(回车后应该是把命令行输入的内容存在标准文件中?),程序可以从文件中读到,并且永远不会读到文件尾,所以可以无限次循环输入读出。如果指定了文件,那么读取文件直到读到文件尾结束循环。

所谓带缓冲的读取,就是设置好一个固定大小的切片作buffer,每次读满buffer然后停止往下读,使用该buffer读到的内容,然后进入下一次循环,继续往下读,注意,每次循环调f.Read(buf[:]),还有一个游标的效果,自动记住上次读到哪,然后往下读,因此直接重复调read()即可。

package main

import (
	"flag"
	"fmt"
	"os"
)

func cat(f *os.File) {
	const NBUF = 1024
	var buf [NBUF]byte
	for {
		switch nr, err := f.Read(buf[:]); true { //nr是读到的字节数(带缓冲的读取)

		case nr < 0:
			fmt.Fprintf(os.Stderr, "cat: error reading: %s\n", err.Error())
			os.Exit(1)
		case nr == 0: // EOF 读到文件尾
			return
		case nr > 0: //输出到标准输出流中
			nw, ew := os.Stdout.Write(buf[0:nr])
			// fmt.Println(nr)

			if nw != nr {
				fmt.Fprintf(os.Stderr, "cat: error writing: %s\n", ew.Error())
			}
		}
	}
}

func main() {
	flag.Parse() // Scans the arg list and sets up flags
	if flag.NArg() == 0 {
		cat(os.Stdin) // os.Stdin是一个打开的文件
	}
	for i := 0; i < flag.NArg(); i++ { // 有文件名时先打开文件再输出
		f, err := os.Open(flag.Arg(i))
		if f == nil {
			fmt.Fprintf(os.Stderr, "cat: can't open %s: error %s\n", flag.Arg(i), err)
			os.Exit(1)
		}
		cat(f)
		f.Close()
	}
}

用 defer 关闭文件

这里的defer可以保证执行完string(contents)后执行文件关闭

func data(name string) string {
	f, _ := os.OpenFile(name, os.O_RDONLY, 0)
	defer f.Close() // idiomatic Go code!
	contents, _ := ioutil.ReadAll(f)
	return string(contents)
}

使用接口的实际例子:fmt.Fprintf

Fprintf内部会调第一个参数的Write()方法,因此第一个传参必须实现write()方法
bufio的结构体bufio.Writer实现了Write()方法,可以传给Fprintf()
Fprintf() 能够写入任何类型,只要其实现了 Write 方法,包括 os.Stdout,文件(例如 os.File),管道,网络连接,通道等等。

// interfaces being used in the GO-package fmt
package main

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

func main() {
	// unbuffered
	fmt.Fprintf(os.Stdout, "%s\n", "hello world! - unbuffered")//向文件输出
	// buffered: os.Stdout implements io.Writer
	buf := bufio.NewWriter(os.Stdout)
	// and now so does buf.
	fmt.Fprintf(buf, "%s\n", "hello world! - buffered")
	buf.Flush() // 必须刷到文件中
}

输出:
hello world! - unbuffered
hello world! - buffered

它还有一个工厂函数:传给它一个 io.Writer 类型的参数,它会返回一个带缓冲的 bufio.Writer 类型的 io.Writer :
func NewWriter(wr io.Writer) (b *Writer)

Writer是bufio自己定义的结构

我基于自定义文件改造的一个程序

// interfaces being used in the GO-package fmt
package main

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

func main() {
	// unbuffered
	f, err := os.OpenFile("C:/Users/Administrator/Desktop/abc.txt", os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		fmt.Println("error")
	}
	fmt.Fprintf(f, "%s\n", "hello world! - unbuffered")
	// buffered: os.Stdout implements io.Writer
	buf := bufio.NewWriter(f)
	// and now so does buf.
	fmt.Fprintf(buf, "%s\n", "hello world! - buffered")
	buf.Flush()
}

练习

下面的代码有一个输入文件 goprogram,然后以每一行为单位读取,从读取的当前行中截取第 3 到第 5 的字节写入另一个文件。然而当你运行这个程序,输出的文件却是个空文件。找出程序逻辑中的 bug,修正它并测试。

package main

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

func main() {
	inputFile, _ := os.Open("C:/Users/Administrator/Desktop/abc.txt")
	outputFile, _ := os.OpenFile("C:/Users/Administrator/Desktop/abc_copy.txt", os.O_WRONLY|os.O_CREATE, 0666)
	defer inputFile.Close()
	defer outputFile.Close()
	inputReader := bufio.NewReader(inputFile)
	outputWriter := bufio.NewWriter(outputFile)
	for {
		inputString, _, readerError := inputReader.ReadLine()
		if readerError == io.EOF {
			fmt.Println("EOF")
			return
		}
		outputString := string(inputString[2:5]) + "\r\n"
		_, err := outputWriter.WriteString(outputString)
		if err != nil {
			fmt.Println(err)
			return
		}
	}
	fmt.Println("Conversion done")
}

abc.txt文件内容:
hello world!
hello world!
hello world!
hello world!
hello world!
hello world!

abc_copy.txt预期应如下:
llo
llo
llo
llo
llo
llo

package main

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

func main() {
	inputFile, _ := os.Open("C:/Users/Administrator/Desktop/abc.txt")
	outputFile, _ := os.OpenFile("C:/Users/Administrator/Desktop/abc_copy.txt", os.O_WRONLY|os.O_CREATE, 0666)
	defer inputFile.Close()
	defer outputFile.Close()
	inputReader := bufio.NewReader(inputFile)
	outputWriter := bufio.NewWriter(outputFile)
	for {
		inputString, _, readerError := inputReader.ReadLine()
		if readerError == io.EOF {
			fmt.Println("EOF")
			return
		}
		outputString := string(inputString[2:5]) + "\r\n"
		_, err := outputWriter.WriteString(outputString)
		outputWriter.Flush() //问题所在

		if err != nil {
			fmt.Println(err)
			return
		}
	}
	fmt.Println("Conversion done")
}

JSON数据格式

数据结构 --> 指定格式 = 序列化 或 编码(传输之前)
指定格式 --> 数据结构 = 反序列化 或 解码(传输之后)

json.Marshal()编码后实际上是一个[]byte

// json.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type Address struct {
	Type    string
	City    string
	Country string
}

type VCard struct {
	FirstName string
	LastName  string
	Addresses []*Address
	Remark    string
}

func main() {
	pa := &Address{"private", "Aartselaar", "Belgium"}
	wa := &Address{"work", "Boom", "Belgium"}
	vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
	// fmt.Printf("%v: \n", vc) // {Jan Kersschot [0x126d2b80 0x126d2be0] none}:
	// JSON format:
	js, _ := json.Marshal(vc) // Marshal v. 组织
	fmt.Printf("JSON format: %s", js)
	// using an encoder:
	file, _ := os.OpenFile("C:/Users/Administrator/Desktop/vcard.json", os.O_CREATE|os.O_WRONLY, 0666)
	defer file.Close()
	enc := json.NewEncoder(file) //工厂函数,这是创建json文件用的,普通的编码用Marshal
	err := enc.Encode(vc)
	if err != nil {
		log.Println("Error in encoding json")
	}
}

JSON 与 Go 类型对应(差异)

bool 对应 JSON 的 boolean
float64 对应 JSON 的 number
string 对应 JSON 的 string
nil 对应 JSON 的 null

不是所有的数据都可以编码为 JSON 类型,只有验证通过的数据结构才能被编码:

JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T 是 json 包中支持的任何类型)
Channel,复杂类型和函数类型不能被编码
不支持循环数据结构;它将引起序列化进入一个无限循环
指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)

(我认为还是把数据结构留在Go里,对外提供JSON时保持简洁)

解码

json.Unmarshal() 的函数签名是 func Unmarshal(data []byte, v interface{}) error 把 JSON 解码为数据结构。

虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。

// json.go
package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	Type    string
	City    string
	Country string
}

type VCard struct {
	FirstName string
	LastName  string
	Addresses []*Address
	Remark    string
}

func main() {
	pa := &Address{"private", "Aartselaar", "Belgium"}
	wa := &Address{"work", "Boom", "Belgium"}
	vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
	vc2 := new(VCard)
	js, _ := json.Marshal(vc)
	fmt.Printf("JSON format: %s", js)
	err := json.Unmarshal(js, vc2)
	fmt.Printf("JSON format: %v", vc2)
	if err != nil {
		fmt.Println("error")
	}
}

解码任何数据 - 不知道结构

补充知识:见第11章,类型断言:如何检测和转换接口变量的类型

假设有一个任意数据,b := []byte({"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}) 我们不了解他实际结构会是如何

// json.go
package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var f interface{}
	b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`) //反引号应该是保持字符串原样的作用
	json.Unmarshal(b, &f)

	m := f.(map[string]interface{}) //如果转换成功,m将会是map[string]interface{}类型的值

	for k, v := range m {
		switch vv := v.(type) {
		case string:
			fmt.Println(k, "is string", vv)
		case int:
			fmt.Println(k, "is int", vv)

		case []interface{}:
			fmt.Println(k, "is an array:")
			for i, u := range vv {
				fmt.Println(i, u)
			}
		default:
			fmt.Println(k, "is of a type I don’t know how to handle")
		}
	}
}

解码任何数据 - 知道结构

如果我们事先知道收到的json的结构,那么就不用上面那么麻烦,那么我们可以定义一个适当的结构并对 JSON 数据反序列化

// json.go
package main

import (
	"encoding/json"
	"fmt"
)

type FamilyMember struct {
	Name    string
	Age     int
	Parents []string
}

func main() {
	var m FamilyMember
	b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`) //反引号应该是保持字符串原样的作用
	json.Unmarshal(b, &m)

	fmt.Println(m)
}

编码和解码流

json 包提供 Decoder 和 Encoder 类型来支持常用 JSON 数据流读写。NewDecoder() 和 NewEncoder() 函数分别封装了 io.Reader 和 io.Writer 接口。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
要想把 JSON 直接写入文件,可以使用 json.NewEncoder 初始化文件(或者任何实现 io.Writer 的类型),并调用 Encode();反过来与其对应的是使用 json.NewDecoder 和 Decode() 函数:

func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error
来看下接口是如何对实现进行抽象的:数据结构可以是任何类型,只要其实现了某种接口,目标或源数据要能够被编码就必须实现 io.Writer 或 io.Reader 接口。由于 Go 语言中到处都实现了 Reader 和 Writer,因此 Encoder 和 Decoder 可被应用的场景非常广泛,例如读取或写入 HTTP 连接、websockets 或文件。

(大概意思就说,任何一个实现了 Reader or Writer的类型都可以传给json的NewDecoder or NewEncoder,然后一执行Encode 或 Decode 就能够把json 拿回来或写出去)

xml

与json差不多,xml也有一个包:encoding/xml

// xml.go
package main

import (
	"encoding/xml"
	"fmt"
	"strings"
)

var t, token xml.Token
var err error

func main() {
	input := "<Person><FirstName country='USA'>Laura</FirstName><LastName>Lynn</LastName></Person>"
	inputReader := strings.NewReader(input) // 读字符串
	p := xml.NewDecoder(inputReader)

	for t, err = p.Token(); err == nil; t, err = p.Token() { //Token是xml标签, Token() 方法返回输入流里的下一个 XML token
		switch token := t.(type) {
		case xml.StartElement:
			name := token.Name.Local // 标签的名字,如Person
			fmt.Printf("Token name: %s\n", name)
			for _, attr := range token.Attr { //属性切片
				attrName := attr.Name.Local //属性键
				attrValue := attr.Value     //属性值
				fmt.Printf("An attribute is: %s %s\n", attrName, attrValue)
				// ...
			}
		case xml.EndElement:
			fmt.Println("End of token")
		case xml.CharData: // 从开始标签到结束标签之间的实际文本
			content := string([]byte(token))
			fmt.Printf("This is the content: %v\n", content)
			// ...
		default:
			// ...
		}
	}
}

用 Gob 传输数据

Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写)。类似于 Python 的 "pickle" 和 Java 的 "Serialization"。

Gob 通常用于远程方法调用(RPCs,参见 15.9 节的 rpc 包)参数和结果的传输,以及应用程序和机器之间的数据传输。 它和 JSON 或 XML 有什么不同呢?Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。 Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 JSON 和 XML 那样的文本格式。 Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。
(总结就是用于 rpc传输参数和结果,底层实现依赖Go语言,因此不能脱离Go使用,它不是xml和json的替代品)

//TODO:Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容?(是不是说,任何Gob都可以被解码?)

只有可导出的字段会被编码,零值会被忽略。
在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。并且还提供了很大的灵活性,比如在发送者看来,整数被编码成没有固定长度的可变长度,而忽略具体的 Go 类型。

// gob1.go
package main

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"log"
)

type P struct {
	X, Y, Z int
	Name    string
}

type Q struct {
	X, Y, Z *int32
	Name    string
}

type Q2 struct {
	X, Y *int32
	Name string
}

func main() {
	// Initialize the encoder and decoder.  Normally enc and dec would be
	// bound to network connections and the encoder and decoder would
	// run in different processes.
	var network bytes.Buffer        // Stand-in for a network connection
	enc := gob.NewEncoder(&network) // Will write to network.
	dec := gob.NewDecoder(&network) // Will read from network.
	// Encode (send) the value.
	err := enc.Encode(P{3, 4, 1, "Pythagora"}) //若Z是0,则不会被传递,只有可导出的字段会被编码,零值会被忽略
	if err != nil {
		log.Fatal("encode error:", err)
	}
	// Decode (receive) the value.
	var q Q
	err = dec.Decode(&q)
	fmt.Printf("%q: {%d,%d,%d}\n", q.Name, *(q.X), *(q.Y), *(q.Z))

	//重复调用Decode会出问题,因为它是往下走的

	err = enc.Encode(P{3, 4, 1, "Pythagoras"})
	if err != nil {
		log.Fatal("encode error:", err)
	}
	var q2 Q2
	err = dec.Decode(&q2)
	if err != nil {
		log.Fatal("decode error:", err)
	}
	fmt.Printf("%q: {%d,%d}\n", q2.Name, *(q2.X), *(q2.Y))
}

// Output:   "Pythagora": {3,4,1}
// 			 "Pythagoras": {3,4}

我们把示例 12.12 的信息写进名为 vcard.gob 的文件作为例子。这会产生一个文本可读数据和二进制数据的混合,当你试着在文本编辑中打开的时候会看到。

练习

向一个文件输出编码,再读回解码

// gob2.go
package main

import (
	"encoding/gob"
	"fmt"
	"io"
	"log"
	"os"
)

type Address struct {
	Type    string
	City    string
	Country string
}

type VCard struct {
	FirstName string
	LastName  string
	Addresses []*Address
	Remark    string
}

var content string

func main() {
	pa := &Address{"private", "Aartselaar", "Belgium"}
	wa := &Address{"work", "Boom", "Belgium"}
	vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
	// fmt.Printf("%v: \n", vc) // {Jan Kersschot [0x126d2b80 0x126d2be0] none}:
	// using an encoder:
	file, _ := os.OpenFile("C:/Users/Administrator/Desktop/abc.md", os.O_CREATE|os.O_WRONLY, 0666)
	defer file.Close()
	enc := gob.NewEncoder(file)

	for i := 0; i < 50; i++ {
		err := enc.Encode(vc)
		if err != nil {
			log.Println("Error in encoding gob")
		}
	}

	vcD := new(VCard)

	// vcD的容量限制每次解码的量,每次Decode会基于上次Decode的位置往下走,因此我们需要一个循环
	file2, _ := os.Open("C:/Users/Administrator/Desktop/abc.md")
	dec := gob.NewDecoder(file2)

	for {
		err := dec.Decode(&vcD)
		fmt.Println(vcD)

		if err == io.EOF {
			break
		}

		if err != nil {
			log.Println("Error in decoding gob")
			break
		}
	}
}

Go 中的密码学

通过网络传输的数据必须加密,以防止被 hacker(黑客)读取或篡改,并且保证发出的数据和收到的数据检验和一致。 鉴于 Go 母公司的业务,我们毫不惊讶地看到 Go 的标准库为该领域提供了超过 30 个的包

hash 包:实现了 adler32、crc32、crc64 和 fnv 校验;
crypto 包:实现了其它的 hash 算法,比如 md4、md5、sha1 等。以及完整地实现了 aes、blowfish、rc4、rsa、xtea 等加密算法。

以下是hash的用法示例,使用sha1生成内容hash,然后传给对方来比对内容是否被篡改。
通过 io.WriteString 或 hasher.Write 将给定的 []byte 附加到当前的 hash.Hash 对象中。

// hash_sha1.go
package main

import (
	"crypto/sha1"
	"fmt"
	"io"
	"log"
)

func main() {
	//通过调用 sha1.New() 创建了一个新的 hash.Hash 对象,用来计算 SHA1 校验值
	hasher := sha1.New()
	io.WriteString(hasher, "test") // 附加内容,之前都是使用bufio的writer调WriteString
	b := []byte("")
	fmt.Printf("Result: %x\n", hasher.Sum(b)) //%x 和 %X 用于格式化 16 进制表示的数字
	fmt.Printf("Result: %d %d\n", hasher.Sum(b), len(hasher.Sum(b)))

	hasher.Reset()
	data := []byte("We shall overcome!")
	n, err := hasher.Write(data) // 另一种附加内容的方式
	if n != len(data) || err != nil {
		log.Printf("Hash write error: %v / %v", n, err)
	}
	checksum := hasher.Sum(b) //校验和
	fmt.Printf("Result: %x\n", checksum)
}

简单实现 CES-CBC 加解密

// hash_sha1.go
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha1"
	"fmt"
	"io"
	"log"
	"strconv"
)

func main() {

	// 校验

	//通过调用 sha1.New() 创建了一个新的 hash.Hash 对象,用来计算 SHA1 校验值
	hasher := sha1.New()
	io.WriteString(hasher, "test") // 附加内容,之前都是使用bufio的writer调WriteString
	b := []byte("")
	fmt.Printf("Result: %x\n", hasher.Sum(b)) //%x 和 %X 用于格式化 16 进制表示的数字
	fmt.Printf("Result: %d %d\n", hasher.Sum(b), len(hasher.Sum(b)))

	hasher.Reset()
	data := []byte("We shall overcome!")
	n, err := hasher.Write(data) // 另一种附加内容的方式
	if n != len(data) || err != nil {
		log.Printf("Hash write error: %v / %v", n, err)
	}
	checksum := hasher.Sum(b) //校验和
	fmt.Printf("Result: %x\n", checksum)

	//加密

	// The key argument should be the AES key, either 16, 24, or 32 bytes
	// to select AES-128, AES-192, or AES-256.
	data2 := []byte("We shall overcome!!!!!!!!!!!!!!!") //内容得是key的整数倍
	key := []byte("abcefg0123456789")
	encry, err := aes.NewCipher(key) //创建秘钥

	if err != nil {
		fmt.Println("NewCipher error")
	}
	blockSize := encry.BlockSize() // 获取秘钥块的长度 - 16
	fmt.Println("秘钥块长度:" + strconv.Itoa(blockSize))

	// 加密模式见https://blog.csdn.net/weixin_42272869/article/details/124278342
	CBCMode := cipher.NewCBCEncrypter(encry, key) //第二个参数是给定一个初始化向量,作为第一个块异或的对象,这里直接使用key
	encrypted := make([]byte, len(data2))
	CBCMode.CryptBlocks(encrypted, data2)
	fmt.Printf("密文:%s \n", encrypted)

	// 解密

	CBCMode2 := cipher.NewCBCDecrypter(encry, key)
	decrypted := make([]byte, len(data2))
	CBCMode2.CryptBlocks(decrypted, encrypted)
	fmt.Printf("明文:%s", decrypted)
}

posted @ 2023-03-01 11:57  有蚊子  阅读(29)  评论(0编辑  收藏  举报