18、Go语言中的数据输入输出

Go语言中的数据输入输出

在Go语言中,数据的输入输出(IO)操作是非常重要的一部分,它涉及到文件读写、标准输入输出、格式化IO等多个方面。下面将详细介绍Go语言中数据IO的相关内容。

1. 使用 io

io 包是Go语言中处理IO操作的基础包,它定义了 io.Reader io.Writer 接口,用于表示输入和输出的基本操作。以下是 io 包中一些常用的函数和类型:
| 函数/类型 | 描述 |
| — | — |
| io.Copy() | 该函数(及其变体 io.CopyBuffer io.CopyN )可以轻松地将数据从任意的 io.Reader 源复制到任意的 io.Writer 目标。示例代码如下:

data := strings.NewReader("Write   me down.")
file, _ := os.Create("./iocopy.data")
io.Copy(file, data)

| PipeReader PipeWriter | io 包中的 PipeReader PipeWriter 类型将IO操作建模为内存中的管道。数据写入管道的 io.Writer ,并可以独立地从管道的 io.Reader 读取。示例代码如下:

file, _ := os.Create("./iopipe.data")
pr, pw := io.Pipe()
go func() {
    fmt.Fprint(pw, "Pipe   streaming")
    pw.Close()
}()
wait := make(chan struct{})
go func() {
    io.Copy(file, pr)
    pr.Close()
    close(wait)
}()
<-wait //wait for pr to finish

需要注意的是,管道写入器会阻塞,直到读取器完全消耗完管道内容或遇到错误。因此,为了避免死锁,读取器和写入器都应该封装在goroutine中。
| io.TeeReader() | 类似于 io.Copy 函数, io.TeeReader 将内容从读取器传输到写入器。不过,该函数还会通过返回的 io.Reader 发出复制的字节(未改变)。 TeeReader 适用于组合多步骤的IO流处理。示例代码如下:

fin, _ := os.Open("./ioteerdr.go")
defer fin.Close()
fout, _ := os.Create("./teereader.gz")
defer fout.Close()
zip := gzip.NewWriter(fout)
defer zip.Close()
sha := sha1.New()
data := io.TeeReader(fin, sha)
io.Copy(zip, data)
fmt.Printf("SHA1 hash %x\n",   sha.Sum(nil))

如果要同时计算SHA - 1和MD5,可以嵌套两个 TeeReader 值,示例代码如下:

sha := sha1.New()
md := md5.New()
data := io.TeeReader(
  io.TeeReader(fin, md), sha,
)
io.Copy(zip, data)

| io.WriteString() | 该函数将字符串的内容写入指定的写入器。示例代码如下:

fout, err := os.Create("./iowritestr.data")
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}
defer fout.Close()
io.WriteString(fout, "Hello   there!\n")

| io.LimitedReader | 该结构体是一个读取器,它只从指定的 io.Reader 读取N个字节。示例代码如下:

str := strings.NewReader("The   quick brown " +
    "fox jumps over the lazy   dog")
limited :=   &io.LimitedReader{R: str, N: 19}
io.Copy(os.Stdout, limited)

运行上述代码将输出: The quick brown fox
| io.SectionReader | 该类型通过指定读取的起始索引(从0开始)和要读取的字节数偏移值来实现查找和跳过操作。示例代码如下:

str := strings.NewReader("The   quick brown"+
    "fox jumps over the lazy   dog")
section := io.NewSectionReader(str,   19, 23)
io.Copy(os.Stdout, section)

运行上述代码将输出: jumps over the lazy dog

2. io/ioutil 子包

io/ioutil 子包实现了一些实用函数,为文件读取、目录列表、临时目录创建和文件写入等IO基本操作提供了便捷的方法。

3. 文件操作

os 包中的 os.File 类型表示系统上的文件句柄,它实现了多个IO基本操作,包括 io.Reader io.Writer 接口,因此可以使用标准的流式IO API处理文件内容。

3.1 创建和打开文件
  • os.Create 函数用于创建一个具有指定路径的新文件。如果文件已经存在, os.Create 将覆盖它。
  • os.Open 函数用于打开一个现有文件进行读取。

以下是一个打开现有文件并复制其内容的示例代码:

func main() {
   f1, err := os.Open("./file0.go")
   if err != nil {
         fmt.Println("Unable to open file:", err)
         os.Exit(1)
   }
   defer f1.Close()
   f2, err := os.Create("./file0.bkp")
   if err != nil {
         fmt.Println("Unable to create file:", err)
         os.Exit(1)
   }
   defer f2.Close()
   n, err := io.Copy(f2, f1)
   if err != nil {
         fmt.Println("Failed to copy:", err)
         os.Exit(1)
   }
   fmt.Printf("Copied %d bytes from %s to %s\n",
       n, f1.Name(), f2.Name())
}
3.2 os.OpenFile 函数

os.OpenFile 函数提供了通用的底层功能,用于创建新文件或打开现有文件,并可以对文件的行为和权限进行细粒度控制。不过,通常更常用 os.Open os.Create 函数,因为它们提供了比 os.OpenFile 函数更简单的抽象。

os.OpenFile 函数接受三个参数:文件路径、表示操作行为的掩码位字段值(例如,只读、读写、截断等)和符合POSIX标准的文件权限值。以下是使用 os.OpenFile 函数重新实现文件复制的示例代码:

func main() {
   f1, err := os.OpenFile("./file0.go", os.O_RDONLY, 0666)
   if err != nil {...}
   defer f1.Close()
   f2, err := os.OpenFile("./file0.bkp", os.O_WRONLY, 0666)
   if err != nil {...}
   defer f2.Close()
   n, err := io.Copy(f2, f1)
   if err != nil {...}
   fmt.Printf("Copied %d bytes from %s to %s\n",
      n, f1.Name(), f2.Name())
}

如果已经有一个操作系统文件描述符的引用,还可以使用 os.NewFile 函数在程序中创建文件句柄。不过, os.NewFile 函数很少使用,因为文件通常使用前面讨论的文件函数进行初始化。

3.3 文件读写
  • 写入文件 :除了使用 os.Copy 函数移动数据外,还可以使用 os.File 变量的 WriteString 方法创建文本文件。示例代码如下:
func main() {
   rows := []string{
         "The quick brown fox",
         "jumps over the lazy dog",
   }
   fout, err := os.Create("./filewrite.data")
   if err != nil {
         fmt.Println(err)
         os.Exit(1)
   }
   defer fout.Close()
   for _, row := range rows {
         fout.WriteString(row)
   }
}

如果数据来源不是文本,可以直接将原始字节写入文件,示例代码如下:

func main() {
   data := [][]byte{
         []byte("The quick brown fox\n"),
         []byte("jumps over the lazy dog\n"),
   }
   fout, err := os.Create("./filewrite.data")
   if err != nil { ... }
   defer fout.Close()
   for _, out := range data {
         fout.Write(out)
   }
}
  • 读取文件 :作为 io.Reader ,可以直接使用 Read 方法从 os.File 类型读取文件内容,将其作为原始字节切片流访问。示例代码如下:
func main() {
   fin, err := os.Open("../ch05/dict.txt")
   if err != nil {
         fmt.Println(err)
         os.Exit(1)
   }
   defer fin.Close()
   p := make([]byte, 1024)
   for {
         n, err := fin.Read(p)
         if err == io.EOF {
               break
         }
         fmt.Print(string(p[:n]))
   }
}
4. 标准输入、输出和错误

os 包包含三个预声明的变量 os.Stdin os.Stdout os.Stderr ,分别表示操作系统的标准输入、输出和错误的文件句柄。以下是一个读取文件并将其内容写入标准输出的示例代码:

func main() {
   f1, err := os.Open("./file0.go")
   if err != nil {
         fmt.Println("Unable to open file:", err)
         os.Exit(1)
   }
   defer f1.Close()
   n, err := io.Copy(os.Stdout, f1)
   if err != nil {
         fmt.Println("Failed to copy:", err)
         os.Exit(1)
   }
   fmt.Printf("Copied %d bytes from %s \n", n, f1.Name())
}
5. 使用 fmt 包进行格式化IO

fmt 包是Go语言中广泛使用的IO包,它提供了一系列用于格式化输入和输出的函数。

5.1 向 io.Writer 接口打印

fmt 包提供了几个函数,用于将文本数据写入任意的 io.Writer 实现。 fmt.Fprint fmt.Fprintln 函数以默认格式写入文本,而 fmt.Fprintf 支持格式说明符。以下是一个使用 fmt.Fprintf 函数将类金属数据以列格式写入指定文本文件的示例代码:

type metalloid struct {
   name   string
   number int32
   weight float64
}
func main() {
   var metalloids = []metalloid{
         {"Boron", 5, 10.81},
         ...
         {"Polonium", 84, 209.0},
   }
   file, _ := os.Create("./metalloids.txt")
   defer file.Close()
   for _, m := range metalloids {
         fmt.Fprintf(
               file,
               "%-10s %-10d %-10.3f\n",
               m.name, m.number, m.weight,
         )
   }
}
5.2 向标准输出打印

fmt.Print fmt.Printf fmt.Println 函数与前面的 Fprint 系列函数具有相同的特性,不同的是它们将文本写入标准输出文件句柄 os.Stdout 。以下是一个将类金属列表写入标准输出的示例代码:

type metalloid struct { ... }
func main() {
   var metalloids = []metalloid{
         {"Boron", 5, 10.81},
         ...
         {"Polonium", 84, 209.0},
   }
   for _, m := range metalloids {
         fmt.Printf(
               "%-10s %-10d %-10.3f\n",
               m.name, m.number, m.weight,
         )
   }
}
5.3 从 io.Reader 读取

fmt 包还支持从 io.Reader 接口格式化读取文本数据。 fmt.Fscan fmt.Fscanln 函数可用于将多个以空格分隔的值读取到指定的参数中。 fmt.Fscanf 函数支持格式说明符,用于更丰富和灵活地解析来自 io.Reader 实现的数据输入。以下是一个使用 fmt.Fscanf 函数格式化输入包含行星数据的空格分隔文件( planets.txt )的示例代码:

func main() {
   var name, hasRing string
   var diam, moons int
   // read data
   data, err := os.Open("./planets.txt")
   if err != nil {
         fmt.Println("Unable to open planet data:", err)
         return
   }
   defer data.Close()
   for {
         _, err := fmt.Fscanf(
               data,
               "%s %d %d %s\n",
               &name, &diam, &moons, &hasRing,
         )
         if err != nil {
               if err == io.EOF {
                     break
               } else {
                     fmt.Println("Scan error:", err)
                     return
               }
         }
         fmt.Printf(
               "%-10s %-10d %-6d %-6s\n",
               name, diam, moons, hasRing,
         )
   }
}
5.4 从标准输入读取

fmt.Scan fmt.Scanf fmt.Scanln 函数用于从标准输入文件句柄 os.Stdin 读取数据。以下是一个从控制台读取文本输入的简单程序示例代码:

func main() {
   var choice int
   fmt.Println("A square is what?")
   fmt.Print("Enter 1=quadrilateral 2=rectagonal:")
   n, err := fmt.Scanf("%d", &choice)
   if n != 1 || err != nil {
         fmt.Println("Follow directions!")
         return
   }
   if choice == 1 {
         fmt.Println("You are correct!")
   } else {
         fmt.Println("Wrong, Google it.")
   }
}
6. 缓冲IO

到目前为止,大多数IO操作都是无缓冲的,这意味着每次读写操作可能会受到底层操作系统处理IO请求延迟的负面影响。而缓冲操作通过在IO操作期间将数据缓冲在内部内存中,减少了延迟。 bufio 包提供了用于缓冲读写IO操作的函数。

6.1 缓冲写入器和读取器
  • 缓冲写入 bufio 包提供了几个函数,用于使用 io.Writer 接口对IO流进行缓冲写入。以下是一个使用缓冲IO创建文本文件并写入内容的示例代码:
func main() {
   rows := []string{
         "The quick brown fox",
         "jumps over the lazy dog",
   }
   fout, err := os.Create("./filewrite.data")
   writer := bufio.NewWriter(fout)
   if err != nil {
         fmt.Println(err)
         os.Exit(1)
   }
   defer fout.Close()
   for _, row := range rows {
         writer.WriteString(row)
   }
   writer.Flush()
}

一般来说, bufio 包中的构造函数通过包装现有的 io.Writer 作为其底层源来创建缓冲写入器。例如,上述代码使用 bufio.NewWriter 函数包装 io.File 变量 fout 创建了一个缓冲写入器。如果要影响内部缓冲区的大小,可以使用 bufio.NewWriterSize(w io.Writer, n int) 构造函数指定内部缓冲区大小。 bufio.Writer 类型还提供了 Write WriteByte 方法用于写入原始字节,以及 WriteRune 方法用于写入Unicode编码字符。
- 缓冲读取 :读取缓冲流可以通过调用 bufio.NewReader 构造函数包装现有的 io.Reader 来实现。以下是一个创建 bufio.Reader 变量并读取文本文件的示例代码:

func main() {
   file, err := os.Open("./bufread0.go")
   if err != nil {
         fmt.Println("Unable to open file:", err)
         return
   }
   defer file.Close()
   reader := bufio.NewReader(file)
   for {
         line, err := reader.ReadString('\n')
         if err != nil {
               if err == io.EOF {
                     break
               } else {
                     fmt.Println("Error reading:, err")
                     return
               }
         }
         fmt.Print(line)
   }
}

上述代码使用 reader.ReadString 方法以 \n 字符作为内容分隔符读取文本文件。如果要影响内部缓冲区的大小,可以使用 bufio.NewReaderSize(w io.Reader, n int) 构造函数指定内部缓冲区大小。 bufio.Reader 类型还提供了 Read ReadByte ReadBytes 方法用于从流中读取原始字节,以及 ReadRune 方法用于读取Unicode编码字符。

6.2 扫描缓冲区

bufio 包还提供了用于扫描和标记来自 io.Reader 源的缓冲输入数据的基本操作。 bufio.Scanner 类型使用 Split 方法定义标记化策略来扫描输入数据。以下是一个使用 bufio.Scanner 重新实现行星示例的代码:

func main() {
   file, err := os.Open("./planets.txt")
   if err != nil {
         fmt.Println("Unable to open file:", err)
         return
   }
   defer file.Close()
   fmt.Printf(
         "%-10s %-10s %-6s %-6s\n",
         "Planet", "Diameter", "Moons", "Ring?",
   )
   scanner := bufio.NewScanner(file)
   scanner.Split(bufio.ScanLines)
   for scanner.Scan() {
         fields := strings.Split(scanner.Text(), " ")
         fmt.Printf(
               "%-10s %-10s %-6s %-6s\n",
               fields[0], fields[1], fields[2], fields[3],
         )
   }
}

使用 bufio.Scanner 的步骤如下:
1. 使用 bufio.NewScanner(io.Reader) 创建一个扫描器。
2. 调用 scanner.Split 方法配置内容的标记化方式。
3. 使用 scanner.Scan 方法遍历生成的标记。
4. 使用 scanner.Text 方法读取标记化的数据。

bufio 包提供了几个预定义的分割器函数,包括 ScanBytes (将每个字节作为一个标记扫描)、 ScanRunes (扫描UTF - 8编码的标记)和 ScanWords (将每个以空格分隔的单词作为标记扫描)。

7. 内存中的IO

bytes 包提供了常见的基本操作,用于对存储在内存中的字节块进行流式IO,由 bytes.Buffer 类型表示。由于 bytes.Buffer 类型实现了 io.Reader io.Writer 接口,因此它是使用流式IO基本操作将数据流入或流出内存的理想选择。以下是一个将多个字符串值存储在 byte.Buffer 变量中,然后将缓冲区流式传输到 os.Stdout 的示例代码:

func main() {
   var books bytes.Buffer
   books.WriteString("The Great Gatsby")
   books.WriteString("1984")
   // 可以继续添加更多字符串
   io.Copy(os.Stdout, &books)
}

通过以上介绍,我们可以看到Go语言提供了丰富的工具和函数,用于处理各种数据IO操作,无论是文件读写、标准输入输出,还是格式化IO和缓冲IO,都能方便地实现。

Go语言中的数据输入输出(续)

8. 总结与操作建议

在前面的内容中,我们详细介绍了Go语言中数据IO的各个方面,下面对不同场景下的操作选择进行总结,并给出具体的操作建议。

8.1 文件操作总结
  • 文件创建与打开
    • 若只是简单创建新文件,优先使用 os.Create 函数,它会覆盖已存在的同名文件。操作步骤为:调用 os.Create 函数并传入文件路径,如 fout, err := os.Create("./filewrite.data") ,若有错误处理错误,使用完文件后通过 defer fout.Close() 关闭文件。
    • 若仅为读取现有文件,使用 os.Open 函数,操作步骤与创建文件类似,如 fin, err := os.Open("./file0.go")
    • 当需要对文件行为和权限进行细粒度控制时,使用 os.OpenFile 函数。操作步骤为:传入文件路径、操作行为掩码位字段值和文件权限值,如 f1, err := os.OpenFile("./file0.go", os.O_RDONLY, 0666) ,后续同样要处理错误和关闭文件。
  • 文件读写
    • 若要将大量数据从一个文件复制到另一个文件,使用 io.Copy 函数,示例代码如下:
f1, err := os.Open("./file0.go")
if err != nil {
    fmt.Println("Unable to open file:", err)
    os.Exit(1)
}
defer f1.Close()
f2, err := os.Create("./file0.bkp")
if err != nil {
    fmt.Println("Unable to create file:", err)
    os.Exit(1)
}
defer f2.Close()
n, err := io.Copy(f2, f1)
if err != nil {
    fmt.Println("Failed to copy:", err)
    os.Exit(1)
}
fmt.Printf("Copied %d bytes from %s to %s\n", n, f1.Name(), f2.Name())
- 若要逐行写入文本数据到文件,可使用`os.File`的`WriteString`方法,如前面的示例:
rows := []string{
    "The quick brown fox",
    "jumps over the lazy dog",
}
fout, err := os.Create("./filewrite.data")
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}
defer fout.Close()
for _, row := range rows {
    fout.WriteString(row)
}
- 若要读取文件内容,可使用`os.File`的`Read`方法按字节块读取,示例代码如下:
fin, err := os.Open("../ch05/dict.txt")
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}
defer fin.Close()
p := make([]byte, 1024)
for {
    n, err := fin.Read(p)
    if err == io.EOF {
        break
    }
    fmt.Print(string(p[:n]))
}
8.2 格式化IO总结
  • io.Writer 写入格式化数据 :使用 fmt.Fprintf 函数,它支持格式说明符。操作步骤为:定义要写入的数据结构,创建 io.Writer 对象(如文件),遍历数据并使用 fmt.Fprintf 写入,示例代码如下:
type metalloid struct {
    name   string
    number int32
    weight float64
}
func main() {
    var metalloids = []metalloid{
        {"Boron", 5, 10.81},
        // ...
        {"Polonium", 84, 209.0},
    }
    file, _ := os.Create("./metalloids.txt")
    defer file.Close()
    for _, m := range metalloids {
        fmt.Fprintf(
            file,
            "%-10s %-10d %-10.3f\n",
            m.name, m.number, m.weight,
        )
    }
}
  • io.Reader 读取格式化数据 :使用 fmt.Fscanf 函数,它支持格式说明符。操作步骤为:打开 io.Reader 对象(如文件),定义接收数据的变量,使用 fmt.Fscanf 按格式读取数据,示例代码如下:
func main() {
    var name, hasRing string
    var diam, moons int
    data, err := os.Open("./planets.txt")
    if err != nil {
        fmt.Println("Unable to open planet data:", err)
        return
    }
    defer data.Close()
    for {
        _, err := fmt.Fscanf(
            data,
            "%s %d %d %s\n",
            &name, &diam, &moons, &hasRing,
        )
        if err != nil {
            if err == io.EOF {
                break
            } else {
                fmt.Println("Scan error:", err)
                return
            }
        }
        fmt.Printf(
            "%-10s %-10d %-6d %-6s\n",
            name, diam, moons, hasRing,
        )
    }
}
8.3 缓冲IO总结
  • 缓冲写入 :使用 bufio.NewWriter 函数创建缓冲写入器。操作步骤为:创建 io.Writer 对象(如文件),使用 bufio.NewWriter 包装该对象,写入数据后调用 Flush 方法刷新缓冲区,示例代码如下:
func main() {
    rows := []string{
        "The quick brown fox",
        "jumps over the lazy dog",
    }
    fout, err := os.Create("./filewrite.data")
    writer := bufio.NewWriter(fout)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer fout.Close()
    for _, row := range rows {
        writer.WriteString(row)
    }
    writer.Flush()
}
  • 缓冲读取 :使用 bufio.NewReader 函数创建缓冲读取器。操作步骤为:打开 io.Reader 对象(如文件),使用 bufio.NewReader 包装该对象,使用读取方法(如 ReadString )读取数据,示例代码如下:
func main() {
    file, err := os.Open("./bufread0.go")
    if err != nil {
        fmt.Println("Unable to open file:", err)
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                break
            } else {
                fmt.Println("Error reading:", err)
                return
            }
        }
        fmt.Print(line)
    }
}
9. 流程图总结

下面通过mermaid流程图展示不同IO操作的大致流程。

9.1 文件复制流程
graph TD;
    A[开始] --> B[打开源文件];
    B --> C{是否打开成功};
    C -- 是 --> D[创建目标文件];
    C -- 否 --> E[处理错误并退出];
    D --> F{是否创建成功};
    F -- 是 --> G[使用io.Copy复制数据];
    F -- 否 --> E;
    G --> H{复制是否成功};
    H -- 是 --> I[关闭源文件和目标文件];
    H -- 否 --> E;
    I --> J[输出复制信息];
    J --> K[结束];
9.2 缓冲写入流程
graph TD;
    A[开始] --> B[创建文件];
    B --> C{是否创建成功};
    C -- 是 --> D[创建缓冲写入器];
    C -- 否 --> E[处理错误并退出];
    D --> F[写入数据到缓冲写入器];
    F --> G[刷新缓冲写入器];
    G --> H[关闭文件];
    H --> I[结束];
10. 不同IO操作对比表格
操作类型 适用场景 优点 缺点 示例函数
无缓冲IO 数据量小、对延迟不敏感的场景 代码简单直接 受底层操作系统IO请求延迟影响大 os.File Read Write 方法
缓冲IO 数据量大、频繁读写的场景 减少底层IO请求次数,降低延迟 增加内存使用 bufio.NewReader bufio.NewWriter
格式化IO 需要对数据进行格式化输入输出的场景 可以方便地处理不同格式的数据 格式说明符使用不当可能导致错误 fmt.Fprintf fmt.Fscanf
内存IO 数据在内存中处理的场景 无需进行磁盘IO,速度快 受内存大小限制 bytes.Buffer

通过以上的总结、流程图和对比表格,我们可以更清晰地了解Go语言中不同数据IO操作的特点和适用场景,在实际开发中能够根据具体需求做出更合适的选择,高效地完成数据IO任务。

提供了一个基于51单片机的RFID门禁系统的完整资源文件,括PCB图、原理图、论文以及源程序。该系统设计由单片机、RFID-RC522频射卡模块、LCD显示、灯控电路、蜂鸣器报警电路、存储模块和按键组成。系统支持通过密码和刷卡两种方式进行门禁控制,灯亮表示开门成功,蜂鸣器响表示开门失败。 资源内容 PCB图:含系统的PCB设计图,方便用户进行硬件电路的制作和调试。 原理图:详细展示了系统的电路连接和模块布局,帮助用户理解系统的工作原理。 论文:提供了系统的详细设计思路、实现方法以及测试结果,适合学习和研究使用。 源程序:含系统的全部源代码,用户可以根据需要进行修改和优化。 系统功能 刷卡开门:用户可以通过刷RFID卡进行门禁控制,系统会自动识别卡片并判断是否允许开门。 密码开门:用户可以通过输入预设密码进行门禁控制,系统会验证密码的正确性。 状态显示:系统通过LCD显示屏显示当前状态,如刷卡成功、密码错误等。 灯光提示:灯亮表示开门成功,灯灭表示开门失败或未操作。 蜂鸣器报警:当刷卡或密码输入错误时,蜂鸣器会发出报警声,提示用户操作失败。 适用人群 电子工程、自动化等相关专业的学生和研究人员。 对单片机和RFID技术感兴趣的爱好者。 需要开发类似门禁系统的工程师和开发者。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值