Go语言学习笔记(十)------读写数据

Go语言学习笔记—golang标准库ioutil包 ioutil包封装了一些I/O实用程序函数。 阅读详情

一、读取用户的输入

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
}

 

Go语言学习笔记—golang并发编程之channel 视频来源于B站Go语言基础进阶视频av56860636 文章为自己整理的学习笔记,侵权即删,谢谢支持! 阅读详情

相关推荐

Go-Readline是一个纯go(golang)实现GNU-Readlinekind库

readline - Readline是一个纯go(golang)实现 GNU-Readline kind 库

Go语言 ReadLine()异常、缓冲区问题

一、前言 近期项目用到了Go语言中的Readline去文件中逐行读取记录,但是有一些异常行信息的长度特别大(超过4K),使用len()过滤后还是有问题,今天问题又复现了,赶紧看看到底是什么问题 二、正文: 直接上测试程序代码: package main import ( "fmt" "bufio" "os" "io" ) func main() { // 打开test.txt文件 fi, err := os.Open("test.txt") if err != nil { fmt.Pr

weixin_43725997的博客 3993

ITK-snap医学图像标注方法(超级详细)——保姆级

医学3D图像标注方法,非常详细,保姆级

jialiyk的博客 3万+

java基础语法概述

1.Java概述 1.1java发展 java语言发展史等信息。 课程使用的jdk版本为11.0。 1.2java跨平台 Java跨平台:程序不能直接运行。 ①java编辑器把java源程序编译为字节码文件(class文件) ②java虚拟机(JVM)对字节码文件解释执行。 1.3JRE和JDK JRE(JAVA运行环境):包含JVM和Java API(核心类库) JDK(Java开发工具):包含...

baidu_33094261的博客 887

Cesium WebGL性能调优实战:从卡顿根源到流畅渲染

本文深入探讨Cesium WebGL性能调优的实战技巧,从硬件配置、框架适配到3D Tiles加载策略,全面解决卡顿问题。通过强制启用独立显卡、优化浏览器设置、避免Vue内存陷阱等实用方法,显著提升3D地图应用的流畅度。文章还分享了高级调试工具和实战避坑经验,帮助开发者实现高效渲染。

weixin_27215337的博客 418

基于ASP.NET+SQLServer的美妆网站的设计与实现

通过本次ASP.NET MVC美妆商城项目的开发,我们实现了一个功能完善、性能稳定的在线购物平台。该项目不仅提升了用户的购物体验,也为商家提供了一个高效的商品管理和销售渠道。未来,我们将持续优化系统功能,提升用户体验,以适应不断变化的市场需求。

毕业设计大牛哥 1497

Golang bufio.Reader ReadLine学习

之前学习go的时候被这个ReadLine坑了一段时间,以为加个for循环就会一行一行的读下去。当时好像调试了半天之类的,总之今天来带你们看看他怎么实现的吧。 首先我们构造一个Reader方便我们调试与分析: sr := strings.NewReader(&quot;123\n456&quot;); reader := bufio.NewReader(sr); line, _, err := reader...

a99361481的博客 1万+

Pytorch学习笔记(III)——提取特征

目录一、引言二、具体步骤1、参数模型2、网络结构3、参数载入4、特征提取器5、读取图片三、完整代码 一、引言   深度学习在许多任务中主要充当着特征学习的作用,而学习完的特征才是后续应用的一个关键。本文将主要介绍,如何提取任意目标层的特征图。   本文以输入数据为图片为例。 二、具体步骤 1、参数模型 博主使用了ResNet50训练了一个人脸识别的网络   训练完成的深度学习模型,我们会保存一...

lockonlxf的博客 2643

JavaScript中闭包和this的指向问题 (思考题)

ES5/6 - 闭包和this的指向问题 (思考题) ⚠️非严格模式 当作为对象的方法调用时,这个函数里的this绑定到这个对象上面, 当作为函数调用时,这个函数里的this绑定到全局对象上面 要求说出以下两段代码在控制台的最终输出结果: (1) var name = 'The Window'; var object = { name:'my object', getNameFunc:function(){ c

Hello World 1708

ROBOGUIDE教程:FANUC机器人X型焊枪气动点焊焊接

FANUC机器人X型焊枪气动点焊焊接虚拟仿真实现方法。

IndRobSim 4384

Python爬虫实战:利用Selenium与Playwright抓取美团外卖数据

在当今大数据时代,网络数据采集已成为商业分析和市场研究的重要手段。本文以美团外卖平台为例,详细介绍如何使用Python最新爬虫技术(Selenium、Playwright)进行数据采集,并探讨相关技术原理、反爬应对策略及数据存储方案。通过5000+字的详细讲解和完整代码示例,帮助读者掌握现代网络爬虫开发的核心技能。本文详细介绍了使用Python最新爬虫技术采集美团外卖数据的完整流程,涵盖了从环境配置、页面分析到数据存储的全链路解决方案。

2201_76125261的博客 2550

Go语言学习笔记(十)I/O

本文介绍了Go语言中的文件读写操作,主要包含以下内容:1.Reader和Writer接口定义及用法;2.文件读取的多种方式,包括单次读取、循环读取和缓冲区读取;3.文件写入的两种模式(覆盖写入和追加写入);4.一次性读写文件的方法;5.缓冲区读写操作及字符串扫描器的使用;6.如何查看文件列表信息。文章通过具体代码示例演示了各种文件操作的使用方法,并展示了执行结果,为Go语言开发者提供了实用的文件操作参考。

不愿言弃-念何的博客 353

Ambari 安装部署教程(手把手教学)【超级详细】

本文为读者提供了一套超详细的 Apache Ambari 安装部署教程,采用手把手教学方式,适合初学者及大数据运维人员。内容涵盖前期准备、环境配置、Ambari Server 与 Agent 安装、集群初始化配置、常见问题排查等全流程操作,确保用户能够顺利搭建稳定的 Hadoop 大数据平台管理系统。

Five的博客 1万+

2024年Go最新Golang中读写CSV文件的全面指南_golang csv(2),2024年最新Golang工作资料

在上述代码中,我们使用os.Open()函数打开一个名为data.csv的文件,并将其传递给函数创建一个CSV Reader对象。注意,我们需要在读取文件后记得关闭文件,以防止资源泄露。Read()Read()在上述代码中,我们使用一个无限循环来持续读取CSV记录,直到遇到文件结束(EOF)为止。每次调用Read()方法,它会返回一个字符串切片,其中每个元素都是CSV记录的一个字段。最后,我们可以在循环中对记录进行处理,如打印、解析等。Write()在上述代码中,我们使用函数创建一个名为。

2401_84919754的博客 1164

ARM功耗管理软件之DVFS&AVS

思考:功耗管理软件栈及示例?WFI&WFE?时钟&电源树?DVFS&AVS?

AI-安全-功耗 1230

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Go语言学习笔记—golang标准库io包

io包中提供I/O原始操作的一系列接口。它主要包装了一些已有的实现,如 os 包中的那些,并将这些抽象成为实用性的功能和一些其他相关的接口。

qq_39280718的博客 2926

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

go语言学习笔记25------读写锁、互斥锁

1锁 为了解决协程同步的问题我们使用了channel,但是GO也提供了传统的同步工具。 它们都在GO的标准库代码包sync和sync/atomic中。 下面我们看一下锁的应用。 什么是锁呢?就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。 1.1死锁 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的...

在路上 5910

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Go语言学习笔记—golang标准库bufio包

bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。

qq_39280718的博客 2389

Go语言学习笔记—golang标准库bytes包

bytes包提供了对字节切片进行读写操作的一系列函数,字节切片处理的函数比较多分为基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数和子切片处理函数等。

qq_39280718的博客 2055

Go语言学习笔记(六)——标准库

Go语言学习笔记

一起交流,一起进步 1715

Go语言学习笔记(一)------目录

做区块链项目,发现自己的Go基础还是比较差,一些基本的东西还是了解不多,所以我想重新过一下go语言,这个系列博客就是为了记录一下自己的学习路径,同时方便自己回顾,在此感谢大家的翻译《the way to go》和无闻大大。 笔记目录 Go语言学习笔记(一)------目录 Go语言学习笔记(二)------Go的基本结构和要素 Go语言学习笔记(三)------控制结构 Go语言学习笔记...

Rcvisual的博客 791

Go语言学习笔记—golang标准库xml包

xml包实现xml解析。

qq_39280718的博客 1952

并发(通道,互斥锁,读写锁。Go语言学习笔记)

并发 定义 并发: 逻辑上具备同时处理多个任务的能力 并行: 物理上在同一时刻执行多个并发任务 通常会说程序是并发设计的,也就是说程序允许多个任务同时执行,但实际上并不一定会在同一时刻发生。在单核处理器上,程序能以间隔方式切换执行。并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。 并行是并发设计的理想执行模式。 多线程或多进程是并行的基本条件,但单线程也可用协程(coroutine)做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发。 协程优点: 将因阻

朱夏彤的博客 4203

Golang学习笔记汇总

前言 Golang,一道春天的闪电 有机会触碰到酷炫的 Golang,感觉特别兴奋。仿佛又和大神们站在了一起,仿佛又有干劲去追逐那些了不起的事情。 在这一系列笔记中,IoT小能手计划先泛泛学习基础知识,快速上手实际项目,以此来达到工作技能基本要求。 语言的学习,有个窍门,不要花10年去练体力内功,上来就得练招式,保持热情,之后再慢慢去琢磨那些并发等抽象复杂的逻辑。 本系列文章作者tw...

IoT小能手 5535

Go语言学习笔记

本文主要涉及Go语言的基础知识,其中interface接口、goroutine协程和channel管道、反射机制等为重点内容。

qq_44599368的博客 1662

Go语言学习笔记(七)——标准库

Go语言学习笔记

一起交流,一起进步 1293
上一篇: Go语言学习笔记(九)------接口(Interfaces)与反射(reflection)
下一篇: Go语言学习笔记(十一)------错误处理与测试
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值