一、读取用户的输入

1.从键盘和标准输入 os.Stdin 读取输入,最简单的办法是使用 fmt 包提供的 Scan 和 Sscan 开头的函数。Scanln 扫描来自标准输入的文本,将空格分隔的值依次存放到后续的参数内,直到碰到换行。 Scanf 与其类似,除了Scanf 的第一个参数用作格式字符串,用来决定如何读取。 Sscan 和以 Sscan 开头的函数则是从字符串读取,除此之外,与 Scanf 相同。如果这些函数读取到的结果与您预想的不同,您可以检查成功读入数据的个数和返回的错误。请看以下程序:

//从控制台读取输入:
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.Println("Please enter your full name: ")
fmt.Scanln(&firstName, &lastName)   //从控制台输入string参数
// fmt.Scanf("%s %s", &firstName, &lastName) //输入格式化字符串
fmt.Printf("Hi %s %s!\n", firstName, lastName) // Hi Chris Naegels
fmt.Sscanf(input, format, &f, &i, &s)          //从字符串读取format形式的内容并赋值f,i,s
fmt.Println("From the string we read: ", f, i, s) //打印到控制台
// 输出结果: From the string we read: 56.12 5212 Go
}

2.使用 bufio 包提供的缓冲读取(buffered reader)来读取数据,inputReader 是一个指向 bufio.Reader 的指针。 inputReader := bufio.NewReader(os.Stdin) 这行代码,将会创建一个读取器,并将其与标准输入绑定,屏幕是标准输出 os.Stdout , os.Stderr 用于显示错误信息,大多数情况下等同于 os.Stdout 。。bufio.NewReader() 构造函数的签名为: func NewReader(rd io.Reader) *Reader,该函数的实参可以是满足 io.Reader 接口的任意对象(任意包含有适当的 Read() 方法),函数返回一个新的带缓冲的 io.Reader 对象,它将从指定读取器(例如 os.Stdin )读取内容。返回的读取器对象提供一个方法 ReadString(delim byte) ,该方法从输入中读取内容,直到碰到 delim 指定的字符,然后将读取到的内容连同 delim 字符一起放到缓冲区。ReadString 返回读取到的字符串,如果碰到错误则返回 nil 。如果它一直读到文件结束,则返回读取到的字符串和io.EOF 。如果读取过程中没有碰到 delim 字符,将返回错误 err != nil 。在下面的例子中,我们会读取键盘输入,直到回车键(\n)被按下。

package main
import (
"fmt"
"bufio"
"os"
)
var inputReader *bufio.Reader //具有reader方法的*bufio.Reader指针
var input string               
var err error
func main() {                     
inputReader = bufio.NewReader(os.Stdin) //将标准输入与读取器绑定,读取内容,返回带缓存reader对象
fmt.Println("Please enter some input: ")
input, err = inputReader.ReadString('\n') //缓存对象到换行符为止
if err == nil {
fmt.Printf("The input was: %s\n", input)  //读取顺利则打印读取内容
}
}

3.从键盘读取输入,使用了 switch 语句:

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")
	}
}

二、文件读写

1.读文件:在 Go 语言中,文件使用指向 os.File 类型的指针来表示的,也叫做文件句柄。我们在前面章节使用到过标准输入os.Stdin 和标准输出 os.Stdout ,他们的类型都是 *os.File 。变量 inputFile 是 *os.File 类型的。该类型是一个结构,表示一个打开文件的描述符(文件句柄)。然后,使用os 包里的 Open 函数来打开一个文件。该函数的参数是文件名,类型为 string 。在上面的程序中,我们以只读模式
打开 input.dat 文件。如果文件不存在或者程序没有足够的权限打开这个文件,Open函数会返回一个错误: inputFile, inputError =
os.Open("input.dat") 。如果文件打开正常,我们就使用 defer inputFile.Close() 语句确保在程序退出前关闭该文件。然后,我们使用 bufio.NewReader 来获得一个读取器变量。通过使用 bufio 包提供的读取器(写入器也类似),如程序所示,我们可以很方便的操作相对高层的 string 对象,而避免了去操作比较底层的字节。接着,我们在一个无限循环中使用 ReadString('\n') 或 ReadBytes('\n') 将文件的内容逐行(行结束符 '\n')读取出来。注意: 在之前的例子中,我们看到,Unix和Linux的行结束符是 \n,而Windows的行结束符是 \r\n。在使用 ReadString和 ReadBytes 方法的时候,我们不需要关心操作系统的类型,直接使用 \n 就可以了。我们也可以使用ReadLine() 方法来实现相同的功能。一旦读取到文件末尾,变量 readerError 的值将变成非空(事实上,常量 io.EOF 的值是 true),我们就会执行return 语句从而退出循环。让我们来看看下面这个程序:

package main
import (
	"bufio"
	"fmt"
	"io"
	"os"
)
func main() {
	var err error
	dir, _ := os.Getwd()   //Getwd返回一个对应当前工作目录的根路径.
	fmt.Println("dir:", dir)

	err = os.Chdir("f:/gocode/src/Study/study/golanguage002") //Chdir将当前工作目录修改为dir指定的目录.
	if err != nil{
		return
	}
	dir, _ = os.Getwd()
	fmt.Println("dir:", dir)
	inputFile, inputError := os.Open("input.dat") //打开文件input.dat
	if inputError != nil {
		fmt.Printf("An error occurred on opening the inputfile\n" +
			"Does the file exist?\n" +
			"Have you got acces to it?\n")
		return // exit the function on error
	}
	defer inputFile.Close() //最后执行确保文件正常关闭
	inputReader := bufio.NewReader(inputFile) //将文件与缓冲器绑定
	for {                                      
		inputString, readerError := inputReader.ReadString('\n') //读取至遇到换行符
		fmt.Printf("The input was: %s", inputString)          //打印这一行
		if readerError == io.EOF {                       //读完退出
			return
		}
	}
}

2. 将整个文件的内容读到一个字符串里:如果您想这么做,可以使用 io/ioutil 包里的 ioutil.ReadFile() 方法,该方法第一个返回值的类型是 []byte ,里面存放读取到的内容,第二个返回值是错误,如果没有错误发生,第二个返回值为 nil。请看示例 12.5。类似的,函数
WriteFile() 可以将 []byte 的值写入文件。

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)
func main() {
	var err error
	dir, _ := os.Getwd()   //Getwd返回一个对应当前工作目录的根路径.
	fmt.Println("dir:", dir)
	err = os.Chdir("f:/gocode/src/Study/study/golanguage002") //Chdir将当前工作目录修改为dir指定的目录.
	if err != nil{
		return
	}
	dir, _ = os.Getwd()
	fmt.Println("dir:", dir)
	inputFile := "input.dat" 
	outputFile := "products_copy.txt"
	buf, err := ioutil.ReadFile(inputFile)  //读取文件为[]byte
	if err != nil {
		fmt.Fprintf(os.Stderr, "File Error: %s\n", err)
		// panic(err.Error())
	}
	fmt.Printf("%s\n", string(buf)) //打印[]byte,转换string
	err = ioutil.WriteFile(outputFile, buf, 0644) // 将[]byte写入products_copy.txt文件
	if err != nil {
		panic(err.Error())
	}
}

3.带缓冲的读取:很多情况下,文件的内容是不按行划分的,或者干脆就是一个二进制文件。在这种情况下, ReadString() 就无法使用了,我们可以使用 bufio.Reader 的 Read() ,它只接收一个参数,变量 n 的值表示读取到的字节数.:

buf := make([]byte, 1024)
...
n, err := inputReader.Read(buf)
if (n == 0) { break}

4.按列读取文件中的数据:如果数据是按列排列并用空格分隔的,你可以使用 fmt 包提供的以 FScan 开头的一系列函数来读取他们。请看以下程序,我们将 3 列的数据分别读入变量 v1、v2 和 v3 内,然后分别把他们添加到切片的尾部。原始文件input.dat:

A Fe GO
40 56 45
150 280 356

执行程序:

package main

import (
	"fmt"
	"os"
)
func main() {
	var err error
	dir, _ := os.Getwd()   //Getwd返回一个对应当前工作目录的根路径.
	fmt.Println("dir:", dir)
	err = os.Chdir("f:/gocode/src/Study/study/golanguage002") //Chdir将当前工作目录修改为dir指定的目录.
	if err != nil{
		return
	}
	dir, _ = os.Getwd()
	fmt.Println("dir:", dir)
	file, err := os.Open("input.dat")
	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)
}

输出结果:

dir: F:\gocode\src\Study
dir: f:\gocode\src\Study\study\golanguage002
[A 40 150]
[Fe 56 280]
[GO 45 356]

注意: path 包里包含一个子包叫 filepath ,这个子包提供了跨平台的函数,用于处理文件名和路径。例如 Base() 函数用于获得路径中的最后一个元素(不包含后面的分隔符):

import "path/filepath"
filename := filepath.Base(path)

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

package main
import (
	"fmt"
	"bufio"
"os"
"compress/gzip"
)
func main() {
	var err error
	dir, _ := os.Getwd()   //Getwd返回一个对应当前工作目录的根路径.
	fmt.Println("dir:", dir)
	err = os.Chdir("f:/gocode/src/Study/study/golanguage002") //Chdir将当前工作目录修改为dir指定的目录.
	if err != nil{
		return
	}
	dir, _ = os.Getwd()
	fmt.Println("dir:", dir)
	fName := "products_copy.gzip"
	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)
	}
	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) //打印每一行
	}
}

6.写文件:除了文件句柄,我们还需要 bufio 的 Writer 。我们以只写模式打开文件 output.dat ,如果文件不存在则自动创建:outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666),可以看到, OpenFile 函数有三个参数:文件名、一个或多个标志(使用逻辑运算符“|”连接),使用的文件权限。我们通常会用到以下标志:
os.O_RDONLY :只读
os.O_WRONLY :只写
os.O_CREATE :创建:如果指定文件不存在,就创建该文件。
os.O_TRUNC :截断:如果指定文件已存在,就将该文件的长度截为0。
在读文件的时候,文件的权限是被忽略的,所以在使用 OpenFile 时传入的第三个参数可以用0。而在写文件时,不管是 Unix 还是Windows,都需要使用 0666。然后,我们创建一个写入器(缓冲区)对象:outputWriter := bufio.NewWriter(outputFile)。接着,使用一个 for 循环,将字符串写入缓冲区,写 10 次: outputWriter.WriteString(outputString),缓冲区的内容紧接着被完全写入文件outputWriter.Flush(),如果写入的东西很简单,我们可以使用 fmt.Fprintf(outputFile, "Some test data.\n") 直接将内容写入文件。 fmt 包里的 F 开头的 Print 函数可以直接写入任何 io.Writer ,包括文件。

package main

import (
	"bufio"
	"fmt"
	"os"
)
func main() {
	var err error
	dir, _ := os.Getwd()   //Getwd返回一个对应当前工作目录的根路径.
	fmt.Println("dir:", dir)
	err = os.Chdir("f:/gocode/src/Study/study/golanguage002") //Chdir将当前工作目录修改为dir指定的目录.
	if err != nil{
		return
	}
	dir, _ = os.Getwd()
	fmt.Println("dir:", dir)
    //读取文件,只写和创建,0666是必须的
	outputFile, outputError := os.OpenFile("input.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++ {
		outputWriter.WriteString(outputString)  //写十次
	}
	outputWriter.Flush() //完全写入文件
}

使用 os.Stdout.WriteString("hello, world\n") ,我们可以输出到屏幕。我们以只写模式创建或打开文件"test",我们不使用缓冲区,直接将内容写入文件: f.WriteString( ):

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")
}

三、文件拷贝

拷贝一个文件到另一个文件?最简单的方式就是使用 io 包,注意 defer 的使用:当打开目标文件时发生了错误,那么 defer 仍然能够确保 src.Close() 执行。如果不这么做,文件会一直保持打开状态并占用资源。

// 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)
}

四、从命令行读取参数

1.os 包中有一个 string 类型的切片变量 os.Args ,用来处理一些基本的命令行参数,它在程序启动后读取命令行输入的参数。在命令行加入参数,像这样: os_args John Bill Marc Luke ,将得到这样的输出: Good Morning Alice John Bill Marc Luke。这个命令行参数会放置在切片 os.Args[] 中(以空格分隔),从索引1开始( os.Args[0] 放的是程序本身的路径名字,在本例中是 os_args )。函数 strings.Join 以空格为间隔连接这些参数。

// os_args.go
package main
import (
"fmt"
"os"
"strings"
)
func main() {
who := "Alice "
if len(os.Args) > 1 {
who += strings.Join(os.Args[1:], " ")
}
fmt.Println("Good Morning", who)
}

2.flag 包有一个扩展功能用来解析命令行选项。但是通常被用来替换基本常量,例如,在某些情况下我们希望在命令行给常量一些不一样的值。在 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
}

flag.Parse() 扫描参数列表(或者常量列表)并设置 flag, flag.Arg(i) 表示第i个参数。 Parse() 之后 flag.Arg(i)全部可用, flag.Arg(0) 就是第一个真实的 flag,而不是像 os.Args(0) 放置程序的名字。flag.Narg() 返回参数的数量。解析后 flag 或常量就可用了。 flag.Bool() 定义了一个默认值是 false 的 flag:当在命令行出现了第一个参数(这里是 "n"),flag 被设置成 true (NewLine 是 *bool 类型)。flag 被解引用到
*NewLine ,所以当值是 true 时将添加一个 Newline("\n")。flag.PrintDefaults() 打印 flag 的使用帮助信息,本例中打印的是:
-n=false: print newline。flag.VisitAll(fn func(*Flag)) 是另一个有用的功能:按照字典顺序遍历 flag,并且对每个标签调用 fn。当在命令行(Windows)中执行: echo.exe A B C ,将输出: A B C ;执行 echo.exe -n A B C ,将输出:
A
B
C
每个字符的输出都新起一行,每次都在输出的数据前面打印使用帮助信息: -n=false: print newline 。对于 flag.Bool 你可以设置布尔型 flag 来测试你的代码,例如定义一个 flag processedFlag :
var processedFlag = flag.Bool("proc", false, "nothing processed yet")
在后面用如下代码来测试:
if *processedFlag { // found flag -proc
r = process()
}
要给 flag 定义其它类型,可以使用 flag.Int() , flag.Float64() , flag.String() 。

package main
import (
	"flag" // command line option parser
	"os"
)
//设置命令行flag标志n,值false,使用帮助说明print newline
var NewLine = flag.Bool("n", false, "print newline") // echo -n flag, of type *bool
const (
	Space = " "
	Newline = "\n"
)
func main() {
	flag.PrintDefaults()//flag.PrintDefaults() 打印 flag 的使用帮助信息
	flag.Parse() //flag.Parse() 扫描参数列表(或者常量列表)并设置 flag
	var s string = ""
	for i := 0; i < flag.NArg(); i++ { //遍历所有的命令行参数
		if i > 0 {
			s += " "
			if *NewLine { // -n is parsed, flag becomes true,只要命令行有n,就为true。
				s += Newline //换行
			}
		}
		s += flag.Arg(i) //每一个输出和参数相加
	}
	os.Stdout.WriteString(s)
}

五、用 buffer 读取文件

们结合使用了缓冲读取文件和命令行 flag 解析这两项技术。如果不加参数,那么你输入什么屏幕就打印什么。参数被认为是文件名,如果文件存在的话就打印文件内容到屏幕。命令行执行 cat 测试输出。

package main
import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
)
func cat(r *bufio.Reader) {
	for {
		buf, err := r.ReadBytes('\n')
		if err == io.EOF {
			break
		}
		fmt.Fprintf(os.Stdout, "%s", buf)
	}
	return
}
func main() {
	flag.Parse()
	if flag.NArg() == 0 {
		cat(bufio.NewReader(os.Stdin)) //打印自身
	}
	for i := 0; i < flag.NArg(); i++ {
		f, err := os.Open(flag.Arg(i)) //读取参数(文件名),打开文件
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s:error reading from %s: %s\n", os.Args[0], flag.Arg(i), err.Error())
			continue
		}
		cat(bufio.NewReader(f)) //写入缓存区,调用函数打印文件内容
	}
}

六、用切片读写文件

切片提供了 Go 中处理 I/O 缓冲的标准方式,下面 cat 函数的第二版中,在一个切片缓冲内使用无限 for 循环(直到文件尾部 EOF)读取文件,并写入到标准输出( os.Stdout )。

func cat(f *os.File) {
const NBUF = 512
var buf [NBUF]byte
for {
switch nr, err := f.Read(buf[:]); true {
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:
if nw, ew := os.Stdout.Write(buf[0:nr]); nw != nr {
fmt.Fprintf(os.Stderr, "cat: error writing: %s\n", ew.Error())
}
}
}
}

上面的代码来自于 cat2.go ,使用了 os 包中的 os.File 和 Read 方法; cat2.go 与 cat.go 具有同样的功能。

package main
import (
"flag"
"fmt"
"os"
)
func cat(f *os.File) {
const NBUF = 512
var buf [NBUF]byte
for {
switch nr, err := f.Read(buf[:]); true { //以切片形式读取文件
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:
if nw, ew := os.Stdout.Write(buf[0:nr]); 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)
}
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 关键字对于在函数结束时关闭打开的文件非常有用,例如下面的代码片段:

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)
}

在函数 return 后执行了 f.Close()。

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

fmt.Fprintf() 依据指定的格式向第一个参数内写入字符串,第一个参数必须实现了 io.Writer 接口。 Fprintf() 能够写入任何类型,只要其实现了 Write 方法,包括 os.Stdout ,文件(例如 os.File),管道,网络连接,通道等等,同样的也可以使用 bufio 包中缓冲写入。bufio 包中定义了 type Writer struct{...} 。bufio.Writer 实现了 Write 方法:
func (b *Writer) Write(p []byte) (nn int, err error)
它还有一个工厂函数:传给它一个 io.Writer 类型的参数,它会返回一个带缓冲的 bufio.Writer 类型的 io.Writer :
func NewWriter(wr io.Writer) (b *Writer)
其适合任何形式的缓冲写入。在缓冲写入的最后千万不要忘了使用 Flush() ,否则最后的输出不会被写入。

package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// unbuffered
fmt.Fprintf(os.Stdout, "%s\n", "hello world! - unbuffered") //任何只要有write方法,实现接口io.write都可以调用Fprintf函数
// buffered: os.Stdout implements io.Writer
buf := bufio.NewWriter(os.Stdout) //实现io.write接口
// and now so does buf.
fmt.Fprintf(buf, "%s\n", "hello world! - buffered")
buf.Flush()
}

九、JSON 数据格式

1.数据结构要在网络中传输或保存到文件,就必须对其编码和解码;目前存在很多编码格式:JSON,XML,gob,Google缓冲协议等等。Go 语言支持所有这些编码格式;术语说明:数据结构 --> 指定格式 = 序列化 或 编码 (传输之前)、指定格式 --> 数据格式 = 反序列化 或 解码 (传输之后)。

2.编码也是一样的,只是输出一个数据流(实现了 io.Writer 接口);解码是从一个数据流(实现了 io.Reader)输出到一个数据结构。出于安全考虑,在 web 应用中最好使用 json.MarshalforHTML() 函数,其对数据执行HTML转码,所以文本可以被安全地嵌在 HTML <script> 标签中。json.NewEncoder() 的函数签名是 func NewEncoder(w io.Writer) *Encoder ,返回的Encoder类型的指针可调用方法
Encode(v interface{}) ,将数据对象 v 的json编码写入 io.Writer w 中。示例:

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)
	fmt.Printf("JSON format: %s", js)
	// using an encoder:
	file, _ := os.OpenFile("vcard.json", os.O_CREATE|os.O_WRONLY, 0666) //生成io.write接口
	defer file.Close()
	enc := json.NewEncoder(file)
	err := enc.Encode(vc)
	if err != nil {
		log.Println("Error in encoding json")
	}
}

JSON 与 Go 类型对应如下:bool 对应 JSON 的 booleans、float64 对应 JSON 的 numbers、string 对应 JSON 的 strings、nil 对应 JSON 的 null。不是所有的数据都可以编码为 JSON 类型:只有验证通过的数据结构才能被编码:JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 json 包中支持的任何类型);Channel,复杂类型和函数类型不能被编码、不支持循环数据结构,它将引起序列化进入一个无限循环;指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)。

3.UnMarshal() 的函数签名是 func Unmarshal(data []byte, v interface{}) error 把 JSON 解码为数据结构,对 vc 编码后的数据为 js ,对其解码时,我们首先创建结构 VCard 用来保存解码的数据: var v VCard, 并调用 json.Unmarshal(js, &v) ,解析 []byte 中的 JSON 数据并将结果存入指针 &v 指向的值。虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。

4.解码任意数据:json 包使用 map[string]interface{} 和 []interface{} 储存任意的 JSON 对象和数组;其可以被反序列化为任何的JSON blob 存储到接口值中。来看这个 JSON 数据,被存储在变量 b 中:

b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`)

不用理解这个数据的结构,我们可以直接使用 Unmarshal 把这个数据编码并保存在接口值中:

var f interface{}
err := json.Unmarshal(b, &f)

f 指向的值是一个 map,key 是一个字符串,value 是自身存储作为空接口类型的值:

map[string]interface{} {
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{} {
"Gomez",
"Morticia",
},
}

要访问这个数据,我们可以使用类型断言:

m := f.(map[string]interface{})

我们可以通过 for range 语法和 type switch 来访问其实际类型,通过这种方式,你可以处理未知的 JSON 数据,同时可以确保类型安全:

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")
}
}

5.解码数据到结构:如果我们事先知道 JSON 数据,我们可以定义一个适当的结构并对 JSON 数据反序列化。下面的例子,我们将定义:

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

并对其反序列化:

var m FamilyMember
err := json.Unmarshal(b, &m)

程序实际上是分配了一个新的切片。这是一个典型的反序列化引用类型(指针、切片和 map)的例子。

6.编码和解码流
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.Decoder 和 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 或文件。

10.XML 数据格式

同 json 包一样,也有 Marshal() 和 UnMarshal() 从 XML 中编码和解码数据;但这个更通用,可以从文件中读取和写入(或者任何实现了 io.Reader 和 io.Writer 接口的类型),和 JSON 的方式一样,XML 数据可以序列化为结构,或者从结构反序列化为 XML 数据;这些可以在例子中看到。encoding/xml 包实现了一个简单的 XML 解析器(SAX),用来解析 XML 数据内容。下面的例子说明如何使用解析器:

// xml.go
package main
import (
	"encoding/xml"
	"fmt"
	"strings"
)
var t, token xml.Token
var err error
func main() {
	input := "<Person><FirstName>Laura</FirstName><LastName>Lynn</LastName></Person>"
	inputReader := strings.NewReader(input)//生成io.reader接口
	p := xml.NewDecoder(inputReader) //写入解码器
	for t, err = p.Token(); err == nil; t, err = p.Token() {
		switch token := t.(type) {
		case xml.StartElement:
			name := token.Name.Local
			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:
			// ...
		}
	}
}

包中定义了若干 XML 标签类型:StartElement,Chardata(这是从开始标签到结束标签之间的实际文本),EndElement,Comment,Directive 或 ProcInst。包中同样定义了一个结构解析器: NewParser 方法持有一个 io.Reader(这里具体类型是 strings.NewReader)并生成一个解析器类型的对象。还有一个 Token() 方法返回输入流里的下一个 XML token。在输入流的结尾处,会返回(nil,io.EOF)。XML 文本被循环处理直到 Token() 返回一个错误,因为已经到达文件尾部,再没有内容可供处理了。通过一个 typeswitch 可以根据一些 XML 标签进一步处理。Chardata 中的内容只是一个 []byte,通过字符串转换让其变得可读性强一些。

十一、用 Gob 传输数据

Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写),Gob 通常用于远程方法调用(RPCs)参数和结果的传输,以及应用程序和机器之间的数据传输。它和 JSON 或 XML 有什么不同呢?Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。 Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 JSON 和 XML 那样的文本格式。 Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。只有可导出的字段会被编码,零值会被忽略。在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段,并且还提供了很大的灵活性。假如在发送者这边有一个有结构 T:

type T struct { X, Y, Z int }
var t = T{X: 7, Y: 0, Z: 8}

而在接收者这边可以用一个结构体 U 类型的变量 u 来接收这个值:

type U struct { X, Y *int8 }
var u U

在接收者中,X 的值是7,Y 的值是0(Y的值并没有从 t 中传递过来,因为它是零值),和 JSON 的使用方式一样,Gob 使用通用的 io.Writer 接口,通过 NewEncoder() 函数创建 Encoder 对象并调用Encode() ;相反的过程使用通用的 io.Reader 接口,通过 NewDecoder() 函数创建 Decoder 对象并调用 Decode 。我们把信息写进名为 vcard.gob 的文件作为例子。这会产生一个文本可读数据和二进制数据的混合,当你试着在文本编辑中打开的时候会看到。在示例编解码,并且以字节缓冲模拟网络传输的简单例子:

// gob1.go
package main
import (
	"bytes"
	"fmt"
	"encoding/gob"
	"log"
)
type P struct {
	X, Y, Z int
	Name string
}
type Q 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, 5, "Pythagoras"})
	if err != nil {
		log.Fatal("encode error:", err)
	}
	// Decode (receive) the value.
	var q Q
	err = dec.Decode(&q) //地址
	if err != nil {
		log.Fatal("decode error:", err)
	}
	fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}
// Output: "Pythagoras": {3,4}

生成god文件:

// gob2.go
package main
import (
	"encoding/gob"
	"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("vcard.gob", os.O_CREATE|os.O_WRONLY, 0666)
	defer file.Close()
	enc := gob.NewEncoder(file)
	err := enc.Encode(vc)
	if err != nil {
		log.Println("Error in encoding gob")
	}
}

十二、Go 中的密码学

Go 的标准库为该领域提供了超过 30 个的包:hash 包:实现了 adler32 、 crc32 、 crc64 和 fnv 校验;crypto 包:实现了其它的 hash 算法,比如 md4 、 md5 、 sha1 等。以及完整地实现了aes 、 blowfish 、 rc4 、 rsa 、 xtea 等加密算法。下面的示例用 sha1 和 md5 计算并输出了一些校验值。

// hash_sha1.go
package main
import (
	"fmt"
	"crypto/sha1"
	"io"
	"log"
)
func main() {
	hasher := sha1.New()  //生成io.write接口
	io.WriteString(hasher, "test") //写入接口数据
	b := []byte{}
	fmt.Printf("Result: %x\n", hasher.Sum(b)) //生成16进制哈希
	fmt.Printf("Result: %d\n", hasher.Sum(b)) //10进制哈希
	//
	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)
}

通过调用 sha1.New() 创建了一个新的 hash.Hash 对象,用来计算 SHA1 校验值。 Hash 类型实际上是一个接口,它实现了 io.Writer 接口,通过 io.WriteString 或 hasher.Write 将给定的 []byte 附加到当前的 hash.Hash 对象中:

type Hash interface {
// Write (via the embedded io.Writer interface) adds more data to the running hash.
// It never returns an error.
io.Writer
// Sum appends the current hash to b and returns the resulting slice.
// It does not change the underlying hash state.
Sum(b []byte) []byte
// Reset resets the Hash to its initial state.
Reset()
// Size returns the number of bytes Sum will return.
Size() int
// BlockSize returns the hash's underlying block size.
// The Write method must be able to accept any amount
// of data, but it may operate more efficiently if all writes
// are a multiple of the block size.
BlockSize() int
}