• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Go 语言标准库之 bufio 包

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

bufio 包实现了缓存I/O。它提供了bufio.Readerbufio.Writer类型,其内部分别包装了io.Readerio.Writer对象,同时分别实现了io.Readerio.Writer接口。同时,该包为文本I/O提供了一些便利操作。

bufio 包提供了带缓存功能的 Reader,在从硬盘中读取字节前使用内存缓存,可以节省操作硬盘I/O的时间。在一些情况下它也很有用,比如每次读一个字节,可以一次性的从硬盘中读取缓存大小的数据到内存缓存中,然后读取缓存中的字节,减少硬盘的磨损以及提升性能。

// bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口
type Reader struct {
    buf []byte    // 缓存
    rd  io.Reader // 底层的 io.Reader
    // r:从 buf 中读取的字节(偏移);w:buf 中写入内容的偏移;
    // w - r 是 buf 中可被读的长度(缓存数据的大小),也是 Buffered() 方法的返回值
    r, w         int
    err          error // 读过程中遇到的错误
    lastByte     int   // 最后一次读到的字节值,-1 是无效值
    lastRuneSize int   // 最后一次读到的 Rune 的大小,只有 ReadRune 操作才会修改该值,-1 是无效值
}

实例化

bufio 包提供了两个实例化 Reader 对象的函数。其中,NewReader()函数本质上是调用NewReaderSize()函数。具体如下:

// 创建一个具有默认大小缓存、从 r 读取的 *Reader,默认缓存大小为 4096 字节
func NewReader(r io.Reader) *Reader

// 创建一个具有最少有 size 大小的缓存、从 r 读取的 *Reader
// 如果参数 r 已经是一个具有足够大缓存的 *Reader 类型值,会返回 r
func NewReaderSize(r io.Reader, size int) *Reader

查看NewReader()NewReaderSize()函数源码:

const (
    defaultBufSize = 4096
)

func NewReader(rd io.Reader) *Reader {
    return NewReaderSize(rd, defaultBufSize)
}

func NewReaderSize(rd io.Reader, size int) *Reader {
    // 已经是 bufio.Reader 类型,且缓存大小不小于 size,则直接返回
    b, ok := rd.(*Reader)
    if ok && len(b.buf) >= size {
        return b
    }
    // 缓存大小不会小于 minReadBufferSize (16字节)
    if size < minReadBufferSize {
        size = minReadBufferSize
    }
    r := new(Reader)
    r.reset(make([]byte, size), rd)
    return r
}

读操作方法

// 从底层输入流读取最多 len(p) 个字节数据写入到 p 中,返回读取的字节数 n 和遇到的任何错误 err
// 读取到输入流结尾,n 可能返回非 0,err 返回 nil 或者 io.EOF,但是下次调用肯定返回 (0, io.EOF)
func (b *Reader) Read(p []byte) (n int, err error)

// 读取并返回一个字节。如果没有可用的数据,会返回错误
func (b *Reader) ReadByte() (c byte, err error)

// 还原最近一次读取操作读出的最后一个字节,相当于让读偏移量 r 前移一个字节
// 连续两次 UnreadByte 操作,而中间没有任何读取操作,会返回错误
func (b *Reader) UnreadByte() error

// 读取一个 utf-8 编码的 unicode 码值,返回该码值、其编码长度和可能的错误
// 如果 utf-8 编码非法,读取位置只移动 1 字节,返回 U+FFFD,返回值 size 为 1 而 err 为 nil。如果没有可用的数据,会返回错误
func (b *Reader) ReadRune() (r rune, size int, err error)

// 还原前一次 ReadRune 操作读取的 unicode 码值,相当于让读偏移量 r 前移一个码值长度
// 需要注意 UnreadRune 方法调用前必须调用 ReadRune 方法(从这点看,UnreadRune 比 UnreadByte 严格很多)
func (b *Reader) UnreadRune() error

// 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的切片
// 如果 ReadBytes 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io.EOF)
// 当且仅当 ReadBytes 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)

// 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的字符串
// 如果 ReadString 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io.EOF)
// 当且仅当 ReadString 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误
func (b *Reader) ReadString(delim byte) (line string, err error)

☕️ 示例代码

package main

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

func main() {
    // 打开文件,只读
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 默认缓存区大小为 4096 字节
    reader := bufio.NewReader(file)

    // 读取最多 10 个字节数据到字节切片 b 中
    b := make([]byte, 10)
    n, err := reader.Read(b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read %d bytes: %s\n", n, b)

    // 读取一个字节, 如果读取不成功会返回 Error
    c, err := reader.ReadByte()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read 1 byte: %c\n", c)

    // 还原最近一次读取操作读出的最后一个字节
    err = reader.UnreadByte()
    if err != nil {
        panic(err)
    }

    // 读取一个字符, 如果读取不成功会返回 Error
    r, size, err := reader.ReadRune()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read 1 character, %d bytes:%c\n", size, r)

    // 还原前一次 ReadRune 操作读取的字符
    err = reader.UnreadRune()
    if err != nil {
        panic(err)
    }

    // 读取到分隔符,包含分隔符,返回字节切片
    b, err = reader.ReadBytes('\n')
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read bytes: %s", b)

    // 读取到分隔符,包含分隔符,返回字符串
    str, err := reader.ReadString('\n')
    if err != nil && err != io.EOF {
        panic(err)
    }
    fmt.Printf("Read string: %s", str)
}

// test.txt 内容
// hello world!!!!
// 你好,世界!!!!

// 控制台输出:
// Read 10 bytes: hello worl
// Read 1 byte: d
// Read 1 character,1 bytes:d
// Read bytes: d!!!!
// Read string: 你好,世界!!!!

Peek 方法

// 返回输入流的下 n 个字节,而不会移动读偏移量 r。返回的 []byte 是 buf 的引用,所以只在下一次调用读取操作前合法
// 如果 Peek() 返回的切片长度比 n 小,它也会返会一个错误说明原因。如果 n 比缓存尺寸还大,返回的错误将是 ErrBufferFull
func (b *Reader) Peek(n int) ([]byte, error)

⭐️ 示例代码

package main

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

func main() {
    // 打开文件,只读
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 默认缓存区大小为 4096 字节
    reader := bufio.NewReader(file)

    // 读取下 5 个字节的数据,文件读偏移量不动,返回的字节切片是内部 buf 的引用
    b, err := reader.Peek(5)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Peeked at 5 bytes: %s\n", b)

    // 读取下 5 个字节的数据写入字节切片 b 中,文件读偏移量同时移动
    b = make([]byte, 5)
    n, err := reader.Read(b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read %d bytes: %s\n", n, b)
}

// test.txt 内容:
// hello world!!!!
// 你好,世界!!!!

// 控制台输出:
// Peeked at 5 bytes: hello
// Read 5 bytes: hello

其它方法

// 返回缓存中现有的可读取的字节数
func (b *Reader) Buffered() int

// 实现 io.WriteTo 接口。从底层输入流中读取数据,写入输出流 w 中,返回写入的字节数和遇到的任何错误
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)

// 丢弃缓存中的数据,清除任何错误,将 b 重设为其下层从 r 读取数据
func (b *Reader) Reset(r io.Reader)

✏️ 示例代码

package main

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

func main() {
    // 打开文件,只读
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 默认缓存区大小为 4096 字节
    reader := bufio.NewReader(file)

    // 读取最多 15 个字节数据到字节切片 b 中
    b := make([]byte, 15)
    n, err := reader.Read(b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read %d bytes: %s\n", n, b)

    // 返回缓存中现有的可读取的字节数
    fmt.Printf("Unread %d bytes\n", reader.Buffered())

    // 将缓存区还未读的数据写入到标准输出
    num, err := reader.WriteTo(os.Stdout)
    if err != nil {
        panic(err)
    }
    fmt.Printf("\nWrite %d bytes\n", num)

    // 丢弃缓存中的数据,清除任何错误,重置输入流
    reader.Reset(os.Stdin)
}

// test.txt 内容:
// hello world!!!!
// 你好,世界!!!!

// 控制台输出:
// Read 15 bytes: hello world!!!!
// Unread 29 bytes
//
// 你好,世界!!!!
// Write 29 bytes

bufio.Writer 类型

bufio 包提供了带缓存功能的 Writer,在写字节到硬盘前使用内存缓存,可以节省操作硬盘I/O的时间。在一些情况下它也很有用,比如每次写一个字节,可以把它们攒在内存缓存中,然后一次写入到硬盘中,减少硬盘的磨损以及提升性能。

// bufio.Writer 结构包装了一个 io.Writer 对象,提供缓存功能,同时实现了 io.Writer 接口
type Writer struct {
    err error     // 写过程中遇到的错误
    buf []byte    // 缓存
    n   int       // 当前缓存中的字节数
    wr  io.Writer // 底层的 io.Writer 对象
}

实例化

bufio 包同样提供了两个实例化 Writer 对象的函数。其中,NewWriter()函数本质上是调用NewWriterSize()函数。具体如下:

// 创建一个具有默认大小缓存、写入 w 的 *Writer,默认缓存大小为 4096 字节
func NewWriter(w io.Writer) *Writer

// 创建一个具有最少有 size 尺寸的缓存、写入 w 的 *Writer
// 如果参数 w 已经是一个具有足够大缓存的 *Writer 类型值,会返回 w
func NewWriterSize(w io.Writer, size int) *Writer

查看NewWriter()NewWriterSize()函数源码:

const (
    defaultBufSize = 4096
)

func NewWriter(w io.Writer) *Writer {
    return NewWriterSize(w, defaultBufSize)
}

func NewWriterSize(w io.Writer, size int) *Writer {
    // 已经是 bufio.Writer 类型,且缓存大小不小于 size,则直接返回
    b, ok := w.(*Writer)
    if ok && len(b.buf) >= size {
        return b
    }
    if size <= 0 {
        size = defaultBufSize
    }
    return &Writer{
        buf: make([]byte, size),
        wr:  w,
    }
}

写操作方法

// 将 p 的内容写入缓存,返回写入的字节数。如果返回值 n < len(p),返回一个错误说明原因
func (b *Writer) Write(p []byte) (n int, err error)

// 将单个字节写入缓存
func (b *Writer) WriteByte(c byte) error

// 将一个 unicode 码值写入缓存,返回写入的字节数和可能的错误
func (b *Writer) WriteRune(r rune) (size int, err error)

// 将一个字符串写入缓存,返回写入的字节数。如果返回值 n < len(s),返回一个错误说明原因
func (b *Writer) WriteString(s string) (n int, err error)

// 缓存满时,写操作会自动调用 Flush 方法。该方法将缓存数据刷新到底层的 io.Writer 对象
// 在所有的写操作完成之后,应该调用 Flush 方法使得缓存都写入底层的 io.Writer 对象
func (b *Writer) Flush() error

???? 示例代码

package main

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

func main() {
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 默认缓存区大小为 4096 字节
    writer := bufio.NewWriter(file)

    // 写字节切片到缓存
    n, err := writer.Write([]byte{65, 66, 67})
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bytes written: %d\n", n)

    // 写单个字节到缓存
    err = writer.WriteByte('!')
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bytes written: 1\n")

    // 写单个字符到缓存
    n, err = writer.WriteRune('您')
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bytes written: %d\n", n)

    // 写字符串到缓存
    n, err = writer.WriteString("Hello World")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bytes written: %d\n", n)

    // 将缓存数据刷新到底层的 io.Writer 对象
    writer.Flush()
}

// 文件 test.txt 内容:
// ABC!您Hello World

// 控制台输出:
// Bytes written: 3
// Bytes written: 1
// Bytes written: 3 
// Bytes written: 12

其它方法

// 返回缓存中已使用的字节数
func (b *Writer) Buffered() int

// 返回缓存中还有多少字节未使用
func (b *Writer) Available() int

// 实现了 io.ReaderFrom 接口。从输入流 r 中读取数据,写入底层输出流中,返回读取的字节数和遇到的任何错误
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)

// 丢弃缓存中的数据,清除任何错误,将 b 重设为将其输出写入 w
func (b *Writer) Reset(w io.Writer)

✌ 示例代码

package main

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

func main() {
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 默认缓存区大小为 4096 字节
    writer := bufio.NewWriter(file)

    // 写字符串到缓存
    n, err := writer.WriteString("Hello World\n")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Bytes written: %d\n", n)

    // 检查缓存中的已使用的字节大小
    unFlushedNum := writer.Buffered()
    fmt.Printf("Bytes buffered: %d\n", unFlushedNum)

    // 检查缓存中未使用的字节大小
    availableNum := writer.Available()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Available buffer: %d\n", availableNum)

    // 从输入流 r 读取数据,写入文件 file 中
    r := strings.NewReader("您好!!!")
    num, err := writer.ReadFrom(r)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Read %d bytes\n", num)

    // 检查缓存中的已使用的字节大小
    unFlushedNum = writer.Buffered()
    fmt.Printf("Bytes buffered: %d\n", unFlushedNum)

    // 检查缓存中未使用的字节大小
    availableNum = writer.Available()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Available buffer: %d\n", availableNum)

    // 将缓存数据刷新到底层的 io.Writer 对象
    writer.Flush()

    // 丢弃缓存中的数据,清除任何错误,将 b 重设为将其输出写入 w
    writer.Reset(writer)
}

// 文件 test.txt 内容:
// Hello World
// 您好!!!

// 控制台输出:
// Bytes written: 12
// Bytes buffered: 12    
// Available buffer: 4084
// Read 15 bytes         
// Bytes buffered: 27    
// Available buffer: 4069

bufio.ReadWriter 类型

bufio 包提供ReadWriter结构存储了bufio.Readerbufio.Writer类型的指针(内嵌),同时实现了io.ReadWriter接口。

type ReadWriter struct {
    *Reader
    *Writer
}

// 创建一个新的、将读写操作分派给 r 和 w 的 ReadWriter
func NewReadWriter(r *Reader, w *Writer) *ReadWriter

bufio.Scanner 类型

Scanner 是 bufio 包下的类型,在处理文件中以分隔符分隔的文本时很有用。通常我们使用换行符作为分隔符将文件内容分成多行。在 CSV 文件中,逗号一般作为分隔符。os.File文件可以被包装成bufio.Scanner,它就像一个缓存 Reader。我们会调用Scan()方法去读取下一个分隔符,使用Text()或者Bytes()获取读取的数据。

分隔符可以不是一个简单的字节或者字符,有一个特殊的方法可以实现分隔符的功能,以及将指针移动多少,返回什么数据。如果没有定制的SplitFunc提供,缺省的ScanLines()会使用 newline 字符作为分隔符,其它的分隔函数还包括ScanRunes()ScanWords(),皆在 bufio 包中。

// SplitFunc 类型代表用于对输出作词法分析的分割函数
// 参数 data 是尚未处理的数据的一个开始部分的切片,参数 atEOF 表示是否 Reader 接口不能提供更多的数据。
// 返回值是解析位置前进的字节数,将要返回给调用者的 token 切片,以及可能遇到的错误。如果数据不足以(保证)
// 生成一个完整的 token,例如需要一整行数据但 data 里没有换行符,SplitFunc 可以返回(0, nil, nil)来告
// 诉 Scanner 读取更多的数据写入切片然后用从同一位置起始、长度更长的切片再试一次(调用 SplitFunc 类型函数)。
// 如果返回值 err 非 nil,扫描将终止并将该错误返回给 Scanner 的调用者。
// 除非 atEOF 为真,永远不会使用空切片 data 调用 SplitFunc 类型函数。然而,如果 atEOF 为真,data 却
// 可能是非空的、且包含着未处理的文本。
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

// 下面是 bufio 包定义的用于 Scanner 类型的分割函数(符合 SplitFunc)

// 本函数会将每个字节作为一个 token 返回
func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)

// 本函数会将每个 utf-8 编码的 unicode 码值作为一个 token 返回。
// 本函数返回的 rune 序列和 range 一个字符串的输出 rune 序列相同。错误的 utf-8 编码会翻译为 U+FFFD = 
// "\xef\xbf\xbd",但只会消耗一个字节。调用者无法区分正确编码的 rune 和错误编码的 rune。
func ScanRunes(data []byte, atEOF bool) (advance int, token []byte, err error)

// 本函数会将空白(参见unicode.IsSpace)分隔的片段(去掉前后空白后)作为一个 token 返回。本函数永远不会返回空字符串
func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error)

// 本函数会将每一行文本去掉末尾的换行标记作为一个 token 返回,返回的行可以是空字符串。
// 换行标记为一个可选的回车后跟一个必选的换行符。最后一行即使没有换行符也会作为一个 token 返回
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
// bufio.Scanner 类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行
type Scanner struct {
    r            io.Reader // The reader provided by the client.
    split        SplitFunc // The function to split the tokens.
    maxTokenSize int       // Maximum size of a token; modified by tests.
    token        []byte    // Last token returned by split.
    buf          []byte    // Buffer used as argument to split.
    start        int       // First non-processed byte in buf.
    end          int       // End of data in buf.
    err          error     // Sticky error.
    empties      int       // Count of successive empty tokens.
    scanCalled   bool      // Scan has been called; buffer is in use.
    done         bool      // Scan has finished.
}

// 创建并返回一个从 r 读取数据的 Scanner,默认的分割函数是 ScanLines
func NewScanner(r io.Reader) *Scanner

// 设置该 Scanner 的分割函数。本方法必须在 Scan 之前调用
func (s *Scanner) Split(split SplitFunc)

// 获取当前位置生成的 token(该 token 可以通过 Bytes 或 Text 方法获得),并让 Scanner 的扫描位置移动到下一个 token。
// 当扫描因为抵达输入流结尾或者遇到错误而停止时,本方法会返回 false。在 Scan 方法返回 false 后,Err 方
// 法将返回扫描时遇到的任何错误;除非是 io.EOF,此时 Err 会返回 nil
func (s *Scanner) Scan() bool

// 返回最近一次 Scan 调用生成的 token。底层数组指向的数据可能会被下一次 Scan 的调用重写
func (s *Scanner) Bytes() []byte

// 返回最近一次 Scan 调用生成的 token,会申请创建一个字符串保存 token 并返回该字符串
func (s *Scanner) Text() string

// 返回 Scanner 遇到的第一个非 EOF 的错误
func (s *Scanner) Err() error

✍ 示例代码

分别使用bufio.Readerbufio.Scanner对象读取文件中的数据,一次读取一行:

// 使用 bufio.Reader 对象的 ReadBytes() 或 ReadString() 方法读取文件中的数据,一次读取一行
package main

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

func main() {
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!")
    // 将文件 offset 设置到文件开头
    file.Seek(0, io.SeekStart)

    // 使用 Reader.ReadString() 方法读取文件
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if len(str) != 0 {
            fmt.Printf(str)
        }

        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
    }
}

// 控制台输出
// http://studygolang.com.
// It is the home of gophers.              
// If you are studying golang, welcome you!
// 使用 bufio.Scanner 对象的 Scan() 和 Text()/Bytes() 方法读取文件中的数据,一次读取一行
package main

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

func main() {
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!")
    // 将文件 offset 设置到文件开头
    file.Seek(0, io.SeekStart)

    // 使用 Scanner 对象读取文件
    scanner := bufio.NewScanner(file)
    // Scan(): 获取当前位置生成的 token,移动到下一个 token
    for scanner.Scan() {
        // Text()/Bytes():返回最近一次 Scan 调用生成的 token
        fmt.Println(scanner.Text())
    }
    // Err():返回 Scanner 遇到的第一个非 EOF 的错误
    if err := scanner.Err(); err != nil {
        panic(err)
    }
}

// 控制台输出:
// http://studygolang.com.
// It is the home of gophers.
// If you are studying golang, welcome you!

???? 示例代码

使用bufio.Scanner对象统计文本中有多少个单词(不排除重复):

package main

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

func main() {
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    file.WriteString("hello World gophers")
    // 将文件 offset 设置到文件开头
    file.Seek(0, io.SeekStart)

    scanner := bufio.NewScanner(file)
    // 缺省的分隔函数是 bufio.ScanLines,我们这里使用 ScanWords
    // 也可以定制一个 SplitFunc 类型的分隔函数
    scanner.Split(bufio.ScanWords)

    count := 0
    // Scan(): 获取当前位置生成的 token,移动到下一个 token
    for scanner.Scan() {
        count++
        // Text()/Bytes():返回最近一次 Scan 调用生成的 token
        fmt.Println(scanner.Text())
    }
    // Err():返回 Scanner 遇到的第一个非 EOF 的错误
    if err := scanner.Err(); err != nil {
        panic(err)
    }
    fmt.Println(count)
}

// 控制台输出:
// hello
// World  
// gophers
// 3   

参考

  1. bufio — 缓存 IO
  2. Go语言的30个常用文件操作,总有一个你会用到

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
调试 Go 的代码生成发布时间:2022-07-10
下一篇:
go的精选类库发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap