Golang bufio包源码分析

go 源码1.10.3   bufio.go

 

一、bufio介绍

bufio实现了带缓冲的IO功能,它是在io.Reader和io.Writer接口对象上提供了进一步的封装,从而提供了更加丰富的操作方法。bufio包主要

 

二、bufio.Reader

1.结构体

type Reader struct {
	buf          []byte    //缓冲区的数据
	rd           io.Reader // 底层的io.Reader
	r, w         int       //  r ,w分别表示 buf中读和写的指针位置
	err          error    //记录本次读取的error,后续操作中调用readErr函数后会重置err
	lastByte     int      //记录读取的最后一个字节(用于撤销)
	lastRuneSize int      //记录读取的最后一个字符(Rune)的长度(用于撤销)
}

bufio.Reader封装了io.Reader对象,并提供了带缓冲的功能。

 

2. 初始化方法

func NewReaderSize(rd io.Reader, size int) *Reader {
	// Is it already a Reader?
	b, ok := rd.(*Reader)
	if ok && len(b.buf) >= size {
		return b
	}
	if size < minReadBufferSize {
		size = minReadBufferSize
	}
	r := new(Reader)
	r.reset(make([]byte, size), rd)
	return r
}

         size用于指定缓冲区的大小,如果size小于minReadBufferSize,则重置size的值为minReadBufferSize(16)。如果该rd是*bufio.Reader对象,并且rd的缓冲区大于size,则不会创建Reader对象,而是直接返回原来的rd对象。否则会创建一个*bufio.Reader对象,并指定buf的大小为size。

 

    

const (
	defaultBufSize = 4096
)


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

创建缓冲区默认大小为defaultBufSize(4096)的*bufio.Reader对象

 

3.Reader常用方法

   

func (b *Reader) Size() int { return len(r.buf) }

  Size方法返回底层缓冲区的大小

 

func (b *Reader) Reset(r io.Reader) {
	b.reset(b.buf, r)
}

func (b *Reader) reset(buf []byte, r io.Reader) {
	*b = Reader{
		buf:          buf,
		rd:           r,
		lastByte:     -1,
		lastRuneSize: -1,
	}
}

   Reset方法丢弃所有缓冲数据,重置所有状态,并切换reader到r,从而从r中读取数据。

 

func (b *Reader) fill() {
	// Slide existing data to beginning.
	if b.r > 0 {
		/*
		将buf中未读的数据copy到buf中首部位置
		重置r和w 位置
		*/
		copy(b.buf, b.buf[b.r:b.w])
		b.w -= b.r
		b.r = 0
	}

	if b.w >= len(b.buf) {
		panic("bufio: tried to fill full buffer")
	}

	// Read new data: try a limited number of times.
	/*
	maxConsecutiveEmptyReads是最多尝试次数
	从rd Reader中读取数据到缓冲区buf中,并重置w的位置索引
	*/
	for i := maxConsecutiveEmptyReads; i > 0; i-- {
		n, err := b.rd.Read(b.buf[b.w:])
		if n < 0 {
			panic(errNegativeRead)
		}
		b.w += n
		if err != nil {
			b.err = err
			return
		}
		if n > 0 {
			return
		}
	}
	b.err = io.ErrNoProgress
}

     fill方法用于将缓冲区读满,可以读入的最大长度是:len(buf)-未读的字节数,如果尝试读取了maxConsecutiveEmptyReads(100)次都没有读取到数据,则会返回。

  

func (b *Reader) Peek(n int) ([]byte, error) {
	if n < 0 {
		return nil, ErrNegativeCount
	}

	/*
	如果buf中未读的字节数小于n
	并且buf中未读的字节数小于buf的总大小
	并且err为nil
	满足以上3个条件,则调用fill方法尝试从Reader中读取部分数据块
	*/
	for b.w-b.r < n && b.w-b.r < len(b.buf) && b.err == nil {
		b.fill() // b.w-b.r < len(b.buf) => buffer is not full
	}

	/*
	如果n大于buf的长度,则返回所有未读的内容,和ErrBufferFull的错误信息
	*/
	if n > len(b.buf) {
		return b.buf[b.r:b.w], ErrBufferFull
	}

	// 0 <= n <= len(b.buf)
	var err error
	/*
	如果n大于可读的长度,则返回error信息
	*/
	if avail := b.w - b.r; avail < n {
		// not enough data in buffer
		n = avail
		err = b.readErr()
		if err == nil {
			err = ErrBufferFull
		}
	}
	return b.buf[b.r : b.r+n], err
}

       Peek返回输入流的前n个字节,而不会移动读取位置。该操作不会将数据读出,只是引用,引用的数据在下一次读取操作之前是有效的,如果Peek返回的切片长度比n小,它也会返会一个错误说明原因。如果n比缓冲尺寸还大,返回的错误将是ErrBufferFull。

 

func (b *Reader) Discard(n int) (discarded int, err error) {
	if n < 0 {
		return 0, ErrNegativeCount
	}
	if n == 0 {
		return
	}
	remain := n  //remain记录剩余要跳过的字节长度
	for {
		skip := b.Buffered() //获取buf中可读的数据长度
		if skip == 0 {//如果可读的数据长度为0,则尝试从底层rd  Reader中读取数据到buf中,然后再获取buf中可读的数据长度
			b.fill()
			skip = b.Buffered()
		}
		if skip > remain {//设置skip
			skip = remain
		}
		b.r += skip  //设置r的位置,改变读的索引值r
		remain -= skip
		if remain == 0 { //如果已经跳过n个字节,则返回
			return n, nil
		}
		if b.err != nil {
			return n - remain, b.readErr()
		}
	}
}

       Discard 方法跳过后续的 n 个字节的数据,返回跳过的字节数。如果结果小于 n,将返回错误信息。如果 n 小于缓存中的数据长度,则不会从底层提取数据。

 

func (b *Reader) Read(p []byte) (n int, err error) {
	n = len(p)
	if n == 0 {  //如果p的长度为0,则直接返回
		return 0, b.readErr()
	}
	if b.r == b.w {  //缓冲区没有可读的数据
		if b.err != nil { //如果上次读取有error,则直接返回
			return 0, b.readErr()
		}
		if len(p) >= len(b.buf) { //如果 len(p) >=  缓冲区大小
			// Large read, empty buffer.
			// Read directly into p to avoid copy.
			n, b.err = b.rd.Read(p)  //直接读取rd中的数据到p中
			if n < 0 {
				panic(errNegativeRead)
			}
			if n > 0 {  //有读取的数据
				b.lastByte = int(p[n-1])
				b.lastRuneSize = -1
			}
			return n, b.readErr()
		}
		// One read.
		// Do not use b.fill, which will loop.
		/*
		如果len(p) < 缓冲区大小,将读(r)的位置和写(w)的位置设为0,并从rd中读取数据到缓冲区buf中
		*/
		b.r = 0
		b.w = 0
		n, b.err = b.rd.Read(b.buf)
		if n < 0 {
			panic(errNegativeRead)
		}
		if n == 0 {
			return 0, b.readErr()
		}
		b.w += n  //更新写的位置
	}

	// copy as much as we can
	n = copy(p, b.buf[b.r:b.w])  //将数据copy到p中
	b.r += n  //并更新读的位置
	b.lastByte = int(b.buf[b.r-1])  //记录读取的最后一个字节
	b.lastRuneSize = -1
	return n, nil
}

        Read从Reader对象b中读出数据到p中,n是返回读取的字节数。如果Reader对象b中的缓冲区buf不为空,则只能读取缓冲中的数据,不会从底层的io.Reader中读取数据。

如果b中的缓冲buf为空,则:

1. len(p) >= 缓冲大小,则跳过缓存,直接从底层io.Reader中读出到p总

2.len(p)<缓存大小,则先将数据从底层的io.Reader中读取到缓存中,再从缓存buf读取到p中。

 

func (b *Reader) ReadByte() (byte, error) {
	b.lastRuneSize = -1
	for b.r == b.w {
		if b.err != nil {
			return 0, b.readErr()
		}
		b.fill() // buffer is empty
	}
	c := b.buf[b.r]
	b.r++
	b.lastByte = int(c) //记录读取的最后一个字节
	return c, nil
}

ReadByte 方法读取并返回一个字节的数据,如果没有可读的字节,则返回一个error

 

// UnreadByte unreads the last byte. Only the most recently read byte can be unread.
func (b *Reader) UnreadByte() error {
	if b.lastByte < 0 || b.r == 0 && b.w > 0 {  //条件判断
		return ErrInvalidUnreadByte
	}
	// b.r > 0 || b.w == 0
	if b.r > 0 { //b.r大于0,则减小读的索引
		b.r--
	} else { //如果b.r为0,则将写w的索引设为1
		// b.r == 0 && b.w == 0
		b.w = 1
	}
	b.buf[b.r] = byte(b.lastByte)  //缓冲区buf中添加最后读的一个字节
	b.lastByte = -1  //将lastByte置为-1
	b.lastRuneSize = -1
	return nil
}

       UnreadByte方法撤销最近一次读取操作读取 最后一个字节(只能撤销最后一个字节,多次调用会有问题)。

 

func (b *Reader) ReadRune() (r rune, size int, err error) {

	/*
	当满足:
	1.可读的字节数小于UTFMax(UTFMax是一个UTF字符占用的最大字节数)
	2.可读的数据不够填充一个UTF-8字符
	3.没有error
	4.可读的字节数小于len(b.buf)
	当满足以上几个条件会调用fill来从io.Reader中读出内容到buf中
	*/
	for b.r+utf8.UTFMax > b.w && !utf8.FullRune(b.buf[b.r:b.w]) && b.err == nil && b.w-b.r < len(b.buf) {
		b.fill() // b.w-b.r < len(buf) => buffer is not full
	}
	b.lastRuneSize = -1  //重置lastRuneSize
	if b.r == b.w {  //当没有可读数据
		return 0, 0, b.readErr()
	}
	r, size = rune(b.buf[b.r]), 1
	if r >= utf8.RuneSelf { //不是ascii码
		r, size = utf8.DecodeRune(b.buf[b.r:b.w]) //读取一个字符r和长度size
	}
	b.r += size
	b.lastByte = int(b.buf[b.r-1])  //设置lastByte
	b.lastRuneSize = size  //设置lastRuneSize
	return r, size, nil
}

    ReadRune  读取一个UTF-8字符,并且返回该字符r,该字符的所占的字节数size,以及可能出现的error,如果UTF-8编码的字符无效,则消耗一个字节并返回大小为1的unicode.ReplacementChar(U + FFFD)

 

func (b *Reader) UnreadRune() error {
	if b.lastRuneSize < 0 || b.r < b.lastRuneSize {
		return ErrInvalidUnreadRune
	}
	b.r -= b.lastRuneSize //修改读的索引r
	b.lastByte = -1
	b.lastRuneSize = -1
	return nil
}

      UnreadRune方法撤销上一次读取UTF-8字符的操作

 

func (b *Reader) Buffered() int { return b.w - b.r }

  Buffered方法返回buf中可读的数据长度

 

func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
	for {
		// Search buffer.
		//从buf中未读的数据中查找delim字节
		//如果可以找到则返回buf中的切片,并设置读的索引位置r,并跳出循环
		if i := bytes.IndexByte(b.buf[b.r:b.w], delim); i >= 0 {
			line = b.buf[b.r : b.r+i+1]
			b.r += i + 1
			break
		}

		// Pending error?
		if b.err != nil {  //如果上次读操作出现error,则返回
			line = b.buf[b.r:b.w]
			b.r = b.w
			err = b.readErr()
			break
		}

		// Buffer full?
		if b.Buffered() >= len(b.buf) { //如果buf是满的,则返回整个buf,并设置错误ErrBufferFull
			b.r = b.w
			line = b.buf
			err = ErrBufferFull
			break
		}

		b.fill() // buffer is not full 如果buf没有满,则向buf中读入数据
	}

	// Handle last byte, if any.
	if i := len(line) - 1; i >= 0 {
		b.lastByte = int(line[i])  //设置最后一个字节
		b.lastRuneSize = -1
	}

	return
}

 ReadSlice方法在b中查找delim字节并返回delim及之前的所有数据,返回的是buf的切片

如果找到delim,则返回查找结果,err为nil

如果未找到delim,则:

1.缓存不满,则将缓存填满后再次查找

2.缓存是满的,则返回整个缓存,err返回ErrBufferFull

3.如果未找到delim且遇到错误(通常是io.EOF),则返回缓存中的所有数据和遇到的错误。

因为返回的数据有可能被下一次的读写操作修改,所以大多数操作应该使用ReadBytes或ReadString,它们返回的数据的拷贝

 

func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {
	//通过ReadSlice读取一行内容,返回的是buf的切片,在下次的读写操作时内容可能会改变
	line, err = b.ReadSlice('\n')
	if err == ErrBufferFull {
		// Handle the case where "\r\n" straddles the buffer.
		if len(line) > 0 && line[len(line)-1] == '\r' {
			// Put the '\r' back on buf and drop it from line.
			// Let the next call to ReadLine check for "\r\n".
			if b.r == 0 {
				// should be unreachable
				panic("bufio: tried to rewind past start of buffer")
			}
			b.r--
			line = line[:len(line)-1]
		}
		return line, true, nil
	}

	if len(line) == 0 {
		if err != nil {
			line = nil
		}
		return
	}
	err = nil

	if line[len(line)-1] == '\n' {
		drop := 1
		if len(line) > 1 && line[len(line)-2] == '\r' {
			drop = 2
		}
		line = line[:len(line)-drop]
	}
	return
}

ReadLine方法是一个低水平的行读取操作,大多数情况下,应该使用

ReadBytes('\n')或ReadString('\n'),或者使用Scanner

ReadLine 通过调用 ReadSlice 方法实现,返回的也是缓存的切片。用于读取一行数据,不包括行尾标记(\n 或 \r\n)。

 

func (b *Reader) ReadBytes(delim byte) ([]byte, error) {
	// Use ReadSlice to look for array,
	// accumulating full buffers.
	var frag []byte
	var full [][]byte  //可能多次从底层io.Reader中读取数据
	var err error
	for {//可能多次调用ReadSlice去查找delim字节,直到找到或unexpected error(包括io.EOF)
		var e error
		frag, e = b.ReadSlice(delim)
		if e == nil { // got final fragment
			break
		}
		if e != ErrBufferFull { // unexpected error
			err = e
			break
		}

		// Make a copy of the buffer.
		buf := make([]byte, len(frag))
		copy(buf, frag)
		full = append(full, buf)
	}

	// Allocate new buffer to hold the full pieces and the fragment.
	n := 0
	for i := range full {
		n += len(full[i])
	}
	n += len(frag)

	// Copy full pieces and fragment in.
	buf := make([]byte, n)
	n = 0
	for i := range full {
		n += copy(buf[n:], full[i])
	}
	copy(buf[n:], frag)
	return buf, err
}

ReadBytes 方法从b中读取数据 直到第一次出现delim字节

功能类似ReadSlice,

不同点:

1.只不过返回的是缓存buf的copy,这样当下次读写操作也不会影响返回的内容

2.ReadSlice最多在整个len(buf)中查找,如果未找到返回ErrBufferFull。但是ReadBytes还会继续从io.Reader中读取数据,直到找到该delim字节,或者io.EOF,或者出现unexpected error

也就是ReadSlice最多返回的切片长度为len(buf),而ReadBytes返回的切片长度可能会大于len(buf)

 

func (b *Reader) ReadString(delim byte) (string, error) {
	bytes, err := b.ReadBytes(delim)
	return string(bytes), err
}

ReadString方法功能同ReadBytes,只不过把ReadBytes返回的切片转成字符串

 

func (b *Reader) WriteTo(w io.Writer) (n int64, err error) {
	n, err = b.writeBuf(w)
	if err != nil {
		return
	}

	if r, ok := b.rd.(io.WriterTo); ok {
		m, err := r.WriteTo(w)
		n += m
		return n, err
	}

	if w, ok := w.(io.ReaderFrom); ok {
		m, err := w.ReadFrom(b.rd)
		n += m
		return n, err
	}

	if b.w-b.r < len(b.buf) {
		b.fill() // buffer not full
	}

	for b.r < b.w {
		// b.r < b.w => buffer is not empty
		m, err := b.writeBuf(w)
		n += m
		if err != nil {
			return n, err
		}
		b.fill() // buffer is empty
	}

	if b.err == io.EOF {
		b.err = nil
	}

	return n, b.readErr()
}

WriteTo 实现了 io.WriterTo,可能会多次调用底层Reader的Read方法。

功能:向w中写入数据,  直到底层的io.Reader(即rd)没有可读的数据

 

// writeBuf writes the Reader's buffer to the writer.
func (b *Reader) writeBuf(w io.Writer) (int64, error) {
	n, err := w.Write(b.buf[b.r:b.w])
	if n < 0 {
		panic(errNegativeWrite)
	}
	b.r += n
	return int64(n), err
}

writeBuf方法用于将Reader中buffer中可读的数据写入到该writer中

 

三、bufio.Writer

1.结构体

type Writer struct {
	err error  //
	buf []byte  //缓冲区
	n   int  //下次写入缓冲区的索引
	wr  io.Writer  //底层的io.Writer
}

  bufio.Writer结构退封装了io.Writer,实现带缓冲的功能。

 

2.初始化

func NewWriterSize(w io.Writer, size int) *Writer {
	// Is it already a Writer?
	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,
	}
}

返回一个新的bufio.Writer的对象,size为缓冲区buf的至少长度

1.当w为bufio.Writer,并且其buf的大小大于size,则直接返回该对象

2.如果size小于等于0,则defaultBufSize

3.创建新的Writer并返回

 

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

创建默认缓冲大小的Writer对象,默认缓冲区为defaultBufSize (4096)

 

3.Writer常用方法

func (b *Writer) Size() int { return len(b.buf) }

Size方法返回该Writer的buf大小

 

// Reset discards any unflushed buffered data, clears any error, and
// resets b to write its output to w. 
func (b *Writer) Reset(w io.Writer) {
	b.err = nil
	b.n = 0
	b.wr = w
}

Reset方法用于重置Writer中的io.Writer

 

// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
	if b.err != nil {
		return b.err
	}
	if b.n == 0 {
		return nil
	}
	n, err := b.wr.Write(b.buf[0:b.n])  //将缓冲区的数据写入到底层io.Writer中
	if n < b.n && err == nil {
		err = io.ErrShortWrite
	}
	if err != nil {
		if n > 0 && n < b.n {
			copy(b.buf[0:b.n-n], b.buf[n:b.n])
		}
		b.n -= n
		b.err = err
		return err
	}
	b.n = 0  //重置n
	return nil
}

Flush将缓冲区buf中的数据写入到底层的io.Writer

 

// Available returns how many bytes are unused in the buffer. 
func (b *Writer) Available() int { return len(b.buf) - b.n }

Available方法获取缓冲区buf中可用的字节数

 

// Buffered returns the number of bytes that have been written into the current buffer.
func (b *Writer) Buffered() int { return b.n }

Buffered方法返回缓冲区buf写入的字节数

 

func (b *Writer) Write(p []byte) (nn int, err error) {
	for len(p) > b.Available() && b.err == nil {//如果len(p)的长度大于缓冲区可用的字节数
		var n int
		if b.Buffered() == 0 { //buf中没有写入的数据,说明len(p) > buf的长度,则将p直接写入到底层的io.Writer
			// Large write, empty buffer.
			// Write directly from p to avoid copy.
			n, b.err = b.wr.Write(p)
		} else {
			/*
			如果buf中已经有写入的数据,则将p中的内容追加到缓冲区的尾部。
			可能p中的数据会全部写入到缓冲区中,也能能p中的数据未全部写入到缓冲区中
			*/
			n = copy(b.buf[b.n:], p)
			b.n += n  //移动缓冲区的写的位置n
			b.Flush()  //刷新缓冲区
		}
		nn += n
		p = p[n:]
	}
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], p)
	b.n += n
	nn += n
	return nn, nil
}

Write将p的内容写入到缓冲区中,返回写入的长度

如果nn < len(p),将返回一个error来解释原因

需要保证写入的顺序,如果缓冲区buf中已经有写入的数据,则将p中的数据先写到缓冲区中

 

// WriteByte writes a single byte. 
func (b *Writer) WriteByte(c byte) error {
	if b.err != nil {
		return b.err
	}
	if b.Available() <= 0 && b.Flush() != nil { //如果缓冲区中没有可用的字节数,则调用Flush刷新缓存区,如果出现error,则返回该error
		return b.err
	}
	b.buf[b.n] = c  //将该字节追加到缓冲区中
	b.n++  //更新缓存区写的索引
	return nil
}

WriteByte方法用向Writer中写入一个字节

 

func (b *Writer) WriteRune(r rune) (size int, err error) {
	if r < utf8.RuneSelf { //r是一个字节,则调用WriteByte
		err = b.WriteByte(byte(r))
		if err != nil {
			return 0, err
		}
		return 1, nil
	}
	if b.err != nil {
		return 0, b.err
	}
	n := b.Available()
	if n < utf8.UTFMax {  //如果缓冲区中可用的字节小于UTFMax(字符最多占用4个字节)
		if b.Flush(); b.err != nil {  //刷新缓冲区
			return 0, b.err
		}
		n = b.Available()
		if n < utf8.UTFMax {
			// Can only happen if buffer is silly small.
			return b.WriteString(string(r)) //如果缓冲区的长度小于4,则调用WriteString写入到底层的io.Writer中
		}
	}
	size = utf8.EncodeRune(b.buf[b.n:], r)//将该字符写入到缓冲区中,并返回大小
	b.n += size
	return size, nil
}

WriteRune方法用于向Writer中写入一个字符

 

func (b *Writer) WriteString(s string) (int, error) {
	nn := 0
	for len(s) > b.Available() && b.err == nil {  //如果字符串s大于缓冲区中可用的字节数,则先把缓冲区写满,然后调用Flush刷新
		n := copy(b.buf[b.n:], s)
		b.n += n
		nn += n
		s = s[n:]
		b.Flush()
	}
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], s)  //将s中剩余的写入到缓冲区buf中
	b.n += n
	nn += n
	return nn, nil
}

WriteString方法用于向Writer中写入字符串s

 

// ReadFrom implements io.ReaderFrom.
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {
	if b.Buffered() == 0 { //缓冲区为空
		if w, ok := b.wr.(io.ReaderFrom); ok { //如果wr实现了io.ReaderFrom,则调用ReadFrom将r的数据写入到w中
			return w.ReadFrom(r)
		}
	}
	var m int
	for {
		if b.Available() == 0 { //如果缓冲区已满,则调用Flush刷新缓存
			if err1 := b.Flush(); err1 != nil {
				return n, err1
			}
		}
		nr := 0
		for nr < maxConsecutiveEmptyReads {
			m, err = r.Read(b.buf[b.n:])  //将r中的数据读出的缓冲区中
			if m != 0 || err != nil { //如果有读出的数据,或者err不为nil,则跳出该for循环
				break
			}
			nr++
		}
		if nr == maxConsecutiveEmptyReads { //如果连续尝试读取了100次,都没有读出数据,则返回error
			return n, io.ErrNoProgress
		}
		b.n += m  //改变缓冲区写入的索引
		n += int64(m)
		if err != nil {
			break
		}
	}
	if err == io.EOF {
		// If we filled the buffer exactly, flush preemptively.
		if b.Available() == 0 {
			err = b.Flush()
		} else {
			err = nil
		}
	}
	return n, err
}

ReadFrom实现了io.ReaderFrom,将r中的数据读出到Writer中

 

四、bufil.ReadWriter

// buffered input and output
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
	*Reader
	*Writer
}

// NewReadWriter allocates a new ReadWriter that dispatches to r and w.
func NewReadWriter(r *Reader, w *Writer) *ReadWriter {
	return &ReadWriter{r, w}
}

ReadWriter封装了bufio.Reader和bufio.Writer

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值