bufio 包实现了缓存IO。它包装了 io.Reader 和 io.Writer 对象,创建了另外的Reader和Writer对象,它们也实现了io.Reader和io.Writer接口,不过它们是有缓存的。该包同时为文本I/O提供了一些便利操作。

1.1. 1.4.1 Reader 类型和方法

bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口。

Reader 结构没有任何导出的字段,结构定义如下:

type Reader struct {buf          []byte        // 缓存rd           io.Reader    // 底层的io.Reader// r:从buf中读走的字节(偏移);w:buf中填充内容的偏移;// w - r 是buf中可被读的长度(缓存数据的大小),也是Buffered()方法的返回值r, w         interr          error        // 读过程中遇到的错误lastByte     int        // 最后一次读到的字节(ReadByte/UnreadByte)lastRuneSize int        // 最后一次读到的Rune的大小(ReadRune/UnreadRune)
}

1.1.1. 1.4.1.1 实例化

bufio 包提供了两个实例化 bufio.Reader 对象的函数:NewReader 和 NewReaderSize。其中,NewReader 函数是调用 NewReaderSize 函数实现的:

func NewReader(rd io.Reader) *Reader {// 默认缓存大小:defaultBufSize=4096return NewReaderSize(rd, defaultBufSize)
}

我们看一下NewReaderSize的源码:

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}// 构造一个bufio.Reader实例return &Reader{buf:          make([]byte, size),rd:           rd,lastByte:     -1,lastRuneSize: -1,}
}

1.1.2. 1.4.1.2 ReadSlice、ReadBytes、ReadString 和 ReadLine 方法

之所以将这几个方法放在一起,是因为他们有着类似的行为。事实上,后三个方法最终都是调用ReadSlice来实现的。所以,我们先来看看ReadSlice方法。

ReadSlice方法签名如下:

func (b *Reader) ReadSlice(delim byte) (line []byte, err error)

ReadSlice从输入中读取,直到遇到第一个界定符(delim)为止,返回一个指向缓存中字节的slice,在下次调用读操作(read)时,这些字节会无效。举例说明:

reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))

输出:

the line:http://studygolang.com. the line:It is the home of gophers
It is the home of gophers

从结果可以看出,第一次ReadSlice的结果(line),在第二次调用读操作后,内容发生了变化。也就是说,ReadSlice返回的[]byte是指向Reader中的buffer,而不是copy一份返回。正因为ReadSlice返回的数据会被下次的I/O操作重写,因此许多的客户端会选择使用ReadBytes或者ReadString来代替。读者可以将上面代码中的ReadSlice改为ReadBytes或ReadString,看看结果有什么不同。

注意,这里的界定符可以是任意的字符,可以将上面代码中的'\n'改为'm'试试。同时,返回的结果是包含界定符本身的,上例中,输出结果有一空行就是'\n'本身。

如果ReadSlice在找到界定符之前遇到了error,它就会返回缓存中所有的数据和错误本身(经常是 io.EOF)。如果在找到界定符之前缓存已经满了,ReadSlice会返回bufio.ErrBufferFull错误。当且仅当返回的结果(line)没有以界定符结束的时候,ReadSlice返回err != nil,也就是说,如果ReadSlice返回的结果line不是以界定符delim结尾,那么返回的err也一定不等于nil(可能是bufio.ErrBufferFull或io.EOF)。例子代码:

reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com"),16)
line, err := reader.ReadSlice('\n')
fmt.Printf("line:%s\terror:%s\n", line, err)
line, err = reader.ReadSlice('\n')
fmt.Printf("line:%s\terror:%s\n", line, err)

输出:

line:http://studygola    error:bufio: buffer full
line:ng.com    error:EOF

ReadBytes方法签名如下:

func (b *Reader) ReadBytes(delim byte) (line []byte, err error)

该方法的参数和返回值类型与ReadSlice都一样。 ReadBytes 从输入中读取直到遇到界定符(delim)为止,返回的slice包含了从当前到界定符的内容(包括界定符)。如果ReadBytes在遇到界定符之前就捕获到一个错误,它会返回遇到错误之前已经读取的数据,和这个捕获到的错误(经常是 io.EOF)。跟ReadSlice一样,如果ReadBytes返回的结果line不是以界定符delim结尾,那么返回的err也一定不等于nil(可能是bufio.ErrBufferFull或io.EOF)。

从这个说明可以看出,ReadBytes和ReadSlice功能和用法都很像,那他们有什么不同呢?

在讲解ReadSlice时说到,它返回的[]byte是指向Reader中的buffer,而不是copy一份返回,也正因为如此,通常我们会使用ReadBytes或ReadString。很显然,ReadBytes返回的[]byte不会是指向Reader中的buffer,通过查看源码可以证实这一点。

还是上面的例子,我们将ReadSlice改为ReadBytes:

reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))

输出:

the line:http://studygolang.com. the line:http://studygolang.com. It is the home of gophers

ReadString方法

看一下该方法的源码:

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

它调用了ReadBytes方法,并将结果的[]byte转为string类型。

ReadLine方法签名如下

func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)

ReadLine是一个底层的原始行读取命令。许多调用者或许会使用ReadBytes('\n')或者ReadString('\n')来代替这个方法。

ReadLine尝试返回单独的行,不包括行尾的换行符。如果一行大于缓存,isPrefix会被设置为true,同时返回该行的开始部分(等于缓存大小的部分)。该行剩余的部分就会在下次调用的时候返回。当下次调用返回该行剩余部分时,isPrefix将会是false。跟ReadSlice一样,返回的line只是buffer的引用,在下次执行IO操作时,line会无效。可以将ReadSlice中的例子该为ReadLine试试。

注意,返回值中,要么line不是nil,要么err非nil,两者不会同时非nil。

ReadLine返回的文本不会包含行结尾("\r\n"或者"\n")。如果输入中没有行尾标识符,不会返回任何指示或者错误。

从上面的讲解中,我们知道,读取一行,通常会选择ReadBytes或ReadString。不过,正常人的思维,应该用ReadLine,只是不明白为啥ReadLine的实现不是通过ReadBytes,然后清除掉行尾的\n(或\r\n),它现在的实现,用不好会出现意想不到的问题,比如丢数据。个人建议可以这么实现读取一行:

line, err := reader.ReadBytes('\n')
line = bytes.TrimRight(line, "\r\n")

这样既读取了一行,也去掉了行尾结束符(当然,如果你希望不留行尾结束符,则直接用ReadBytes即可)。

1.1.3. 1.4.1.3 Peek 方法

从方法的名称可以猜到,该方法只是“窥探”一下Reader中没有读取的n个字节。好比栈数据结构中的取栈顶元素,但不出栈。

方法的签名如下:

func (b *Reader) Peek(n int) ([]byte, error)

同上面介绍的ReadSlice一样,返回的[]byte只是buffer中的引用,在下次IO操作后会无效,可见该方法(以及ReadSlice这样的,返回buffer引用的方法)对多goroutine是不安全的,也就是在多并发环境下,不能依赖其结果。

我们通过例子来证明一下:

package mainimport ("bufio""fmt""strings""time"
)func main() {reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com.\t It is the home of gophers"), 14)go Peek(reader)go reader.ReadBytes('\t')time.Sleep(1e8)
}func Peek(reader *bufio.Reader) {line, _ := reader.Peek(14)fmt.Printf("%s\n", line)// time.Sleep(1)fmt.Printf("%s\n", line)
}

输出:

http://studygo
http://studygo

输出结果和预期的一致。然而,这是由于目前的goroutine调度方式导致的结果。如果我们将例子中注释掉的time.Sleep(1)取消注释(这样调度其他goroutine执行),再次运行,得到的结果为:

http://studygo
ng.com.     It is

另外,Reader的Peek方法如果返回的[]byte长度小于n,这时返回的err为非nil,用于解释为啥会小于n。如果n大于reader的buffer长度,err会是ErrBufferFull。

1.1.4. 1.4.1.4 其他方法

Reader的其他方法都是实现了io包中的接口,它们的使用方法在io包中都有介绍,在此不赘述。

这些方法包括:

func (b *Reader) Read(p []byte) (n int, err error)
func (b *Reader) ReadByte() (c byte, err error)
func (b *Reader) ReadRune() (r rune, size int, err error)
func (b *Reader) UnreadByte() error
func (b *Reader) UnreadRune() error
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)

你应该知道它们都是哪个接口的方法吧。

1.2. 1.4.2 Scanner 类型和方法

对于简单的读取一行,在Reader类型中,感觉没有让人特别满意的方法。于是,Go1.1增加了一个类型:Scanner。官方关于Go1.1增加该类型的说明如下:

在 bufio 包中有多种方式获取文本输入,ReadBytes、ReadString 和独特的 ReadLine,对于简单的目的这些都有些过于复杂了。在 Go 1.1 中,添加了一个新类型,Scanner,以便更容易的处理如按行读取输入序列或空格分隔的词等,这类简单的任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都剔除分隔标识。这里的代码展示一次输入一行:

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {fmt.Fprintln(os.Stderr, "reading standard input:", err)
}

输入的行为可以通过一个函数控制,来控制输入的每个部分(参阅 SplitFunc 的文档),但是对于复杂的问题或持续传递错误的,可能还是需要原有接口。

Scanner 类型和 Reader 类型一样,没有任何导出的字段,同时它也包装了一个 io.Reader 对象,但它没有实现 io.Reader 接口。

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

这里 split、maxTokenSize 和 token 需要讲解一下。

然而,在讲解之前,需要先讲解 split 字段的类型 SplitFunc。

1.2.1. 1.4.2.1 SplitFunc 类型和实例

SplitFunc 类型定义如下:

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

SplitFunc 定义了 用于对输入进行分词的 split 函数的签名。参数 data 是还未处理的数据,atEOF 标识 Reader 是否还有更多数据(是否到了EOF)。返回值 advance 表示从输入中读取的字节数,token 表示下一个结果数据,err 则代表可能的错误。

举例说明一下这里的 token 代表的意思:

有数据 "studygolang\tpolaris\tgolangchina",通过"\t"进行分词,那么会得到三个token,它们的内容分别是:studygolang、polaris 和 golangchina。而 SplitFunc 的功能是:进行分词,并返回未处理的数据中第一个 token。对于这个数据,就是返回 studygolang。

如果 data 中没有一个完整的 token,例如,在扫描行(scanning lines)时没有换行符,SplitFunc 会返回(0,nil,nil)通知 Scanner 读取更多数据到 slice 中,然后在这个更大的 slice 中同样的读取点处,从输入中重试读取。如下面要讲解的 split 函数的源码中有这样的代码:

// Request more data.
return 0, nil, nil

如果 err 非nil,扫描停止,同时该错误会返回。

如果参数 data 为空的 slice,除非 atEOF 为 true,否则该函数永远不会被调用。如果 atEOF 为 true,这时 data 可以非空,这时的数据是没有处理的。

bufio 包定义的 split 函数,即 SplitFunc 的实例

在 bufio 包中预定义了一些 split 函数,也就是说,在 Scanner 结构中的 split 字段,可以通过这些预定义的 split 赋值,同时 Scanner 类型的 Split 方法也可以接收这些预定义函数作为参数。所以,我们可以说,这些预定义 split 函数都是 SplitFunc 类型的实例。这些函数包括:ScanBytes、ScanRunes、ScanWords 和 ScanLines。(由于都是 SplitFunc 的实例,自然这些函数的签名都和 SplitFunc 一样)

ScanBytes 返回单个字节作为一个 token。

ScanRunes 返回单个 UTF-8 编码的 rune 作为一个 token。返回的 rune 序列(token)和 range string类型 返回的序列是等价的,也就是说,对于无效的 UTF-8 编码会解释为 U+FFFD = "\xef\xbf\xbd"。

ScanWords 返回通过“空格”分词的单词。如:study golang,调用会返回study。注意,这里的“空格”是 unicode.IsSpace(),即包括:'\t', '\n', '\v', '\f', '\r', ' ', U+0085 (NEL), U+00A0 (NBSP)。

ScanLines 返回一行文本,不包括行尾的换行符。这里的换行包括了Windows下的"\r\n"和Unix下的"\n"。

一般地,我们不会单独使用这些函数,而是提供给 Scanner 实例使用。现在我们回到 Scanner 的 split、maxTokenSize 和 token 字段上来。

split 字段(SplitFunc 类型实例),很显然,代表了当前 Scanner 使用的分词策略,可以使用上面介绍的预定义 SplitFunc 实例赋值,也可以自定义 SplitFunc 实例。(当然,要给 split 字段赋值,必须调用 Scanner 的 Split 方法)

maxTokenSize 字段 表示通过 split 分词后的一个 token 允许的最大长度。在该包中定义了一个常量 MaxScanTokenSize = 64 * 1024,这是允许的最大 token 长度(64k)。

token 字段 上文已经解释了这个是什么意思。

1.2.2. 1.4.2.2 Scanner 的实例化

Scanner 没有导出任何字段,而它需要有外部的 io.Reader 对象,因此,我们不能直接实例化 Scanner 对象,必须通过 bufio 包提供的实例化函数来实例化。实例化函数签名以及内部实现:

func NewScanner(r io.Reader) *Scanner {return &Scanner{r:            r,split:        ScanLines,maxTokenSize: MaxScanTokenSize,buf:          make([]byte, 4096), // Plausible starting size; needn't be large.}
}

可见,返回的 Scanner 实例默认的 split 函数是 ScanLines。

1.2.3. 1.4.2.2 Scanner 的方法

Split 方法 前面我们提到过可以通过 Split 方法为 Scanner 实例设置分词行为。由于 Scanner 实例的默认 split 总是 ScanLines,如果我们想要用其他的 split,可以通过 Split 方法做到。

比如,我们想要统计一段英文有多少个单词(不排除重复),我们可以这么做:

const input = "This is The Golang Standard Library.\nWelcome you!"
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(bufio.ScanWords)
count := 0
for scanner.Scan() {count++
}
if err := scanner.Err(); err != nil {fmt.Fprintln(os.Stderr, "reading input:", err)
}
fmt.Println(count)

输出:

8

我们实例化 Scanner 后,通过调用 scanner.Split(bufio.ScanWords) 来更改 split 函数。注意,我们应该在调用 Scan 方法之前调用 Split 方法。

Scan 方法 该方法好比 iterator 中的 Next 方法,它用于将 Scanner 获取下一个 token,以便 Bytes 和 Text 方法可用。当扫描停止时,它返回false,这时候,要么是到了输入的末尾要么是遇到了一个错误。注意,当 Scan 返回 false 时,通过 Err 方法可以获取第一个遇到的错误(但如果错误是 io.EOF,Err 方法会返回 nil)。

Bytes 和 Text 方法 这两个方法的行为一致,都是返回最近的 token,无非 Bytes 返回的是 []byte,Text 返回的是 string。该方法应该在 Scan 调用后调用,而且,下次调用 Scan 会覆盖这次的 token。比如:

scanner := bufio.NewScanner(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
if scanner.Scan() {scanner.Scan()fmt.Printf("%s", scanner.Text())
}

返回的是:It is the home of gophers 而不是 http://studygolang.com.

Err 方法 前面已经提到,通过 Err 方法可以获取第一个遇到的错误(但如果错误是 io.EOF,Err 方法会返回 nil)。

下面,我们通过一个完整的示例来演示 Scanner 类型的使用。

1.2.4. 1.4.2.3 Scanner 使用示例

我们经常会有这样的需求:读取文件中的数据,一次读取一行。在学习了 Reader 类型,我们可以使用它的 ReadBytes 或 ReadString来实现,甚至使用 ReadLine 来实现。然而,在 Go1.1 中,我们可以使用 Scanner 来做这件事,而且更简单好用。

file, err := os.Create("scanner.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, os.SEEK_SET)
scanner := bufio.NewScanner(file)
for scanner.Scan() {fmt.Println(scanner.Text())
}

输出结果:

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

1.3. 1.4.3 Writer 类型和方法

bufio.Writer 结构包装了一个 io.Writer 对象,提供缓存功能,同时实现了 io.Writer 接口。

Writer 结构没有任何导出的字段,结构定义如下:

type Writer struct {err error        // 写过程中遇到的错误buf []byte        // 缓存n   int            // 当前缓存中的字节数wr  io.Writer    // 底层的 io.Writer 对象
}

相比 bufio.Reader, bufio.Writer 结构定义简单很多。

注意:如果在写数据到 Writer 的时候出现了一个错误,不会再允许有数据被写进来了,并且所有随后的写操作都会返回该错误。

1.3.1. 1.4.3.1 实例化

和 Reader 类型一样,bufio 包提供了两个实例化 bufio.Writer 对象的函数:NewWriter 和 NewWriterSize。其中,NewWriter 函数是调用 NewWriterSize 函数实现的:

func NewWriter(wr io.Writer) *Writer {// 默认缓存大小:defaultBufSize=4096return NewWriterSize(wr, defaultBufSize)
}

我们看一下 NewWriterSize 的源码:

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

1.3.2. 1.4.3.2 Available 和 Buffered 方法

Available 方法获取缓存中还未使用的字节数(缓存大小 - 字段 n 的值);Buffered 方法获取写入当前缓存中的字节数(字段 n 的值)

1.3.3. 1.4.3.3 Flush 方法

该方法将缓存中的所有数据写入底层的 io.Writer 对象中。使用 bufio.Writer 时,在所有的 Write 操作完成之后,应该调用 Flush 方法使得缓存都写入 io.Writer 对象中。

1.3.4. 1.4.3.4 其他方法

Writer 类型其他方法是一些实际的写方法:

// 实现了 io.ReaderFrom 接口
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)// 实现了 io.Writer 接口
func (b *Writer) Write(p []byte) (nn int, err error)// 实现了 io.ByteWriter 接口
func (b *Writer) WriteByte(c byte) error// io 中没有该方法的接口,它用于写入单个 Unicode 码点,返回写入的字节数(码点占用的字节),内部实现会根据当前 rune 的范围调用 WriteByte 或 WriteString
func (b *Writer) WriteRune(r rune) (size int, err error)// 写入字符串,如果返回写入的字节数比 len(s) 小,返回的error会解释原因
func (b *Writer) WriteString(s string) (int, error)

这些写方法在缓存满了时会调用 Flush 方法。另外,这些写方法源码开始处,有这样的代码:

if b.err != nil {return b.err
}

也就是说,只要写的过程中遇到了错误,再次调用写操作会直接返回该错误。

1.4. 1.4.4 ReadWriter 类型和实例化

ReadWriter 结构存储了 bufio.Reader 和 bufio.Writer 类型的指针(内嵌),它实现了 io.ReadWriter 结构。

type ReadWriter struct {*Reader*Writer
}

ReadWriter 的实例化可以跟普通结构类型一样,也可以通过调用 bufio.NewReadWriter 函数来实现:只是简单的实例化 ReadWriter

func NewReadWriter(r *Reader, w *Writer) *ReadWriter {return &ReadWriter{r, w}
}

go 输入输出流(bufio)相关推荐

  1. python输入输出流详解_输入输出流的概念

    Java中的文件复制相较Python而言,涉及到输入输出流的概念,实现中会调用很多对象,复杂很多,在此以文件复制进行简单总结. 这里是一个简单的处理代码: import java.io.*; publ ...

  2. C++ 输入输出流 文本文件 二进制文件读写

    文本文件/ASCII文件(能直接显示内容,费存储空间):文件中每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件或称字符文件. 二进制文件(不能显示内容,节 ...

  3. c++一日一练:让标准的输入输出流关联一个缓冲区

    上篇文章讲述了如何创建自己的标准输入输出,但是如何将标准的输入输出流进行重定向呢: 下面是一个具体的方法: fp = _fdopen( hConHandle, "w" );    ...

  4. Java IO (二),常见的输入/输出流

    字节流和字符流 InputStream和Reader InputStream和Reader两个抽象类是所有输入流的基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板.他们的方法是所有输入 ...

  5. java输出流输入流的使用_Java中的IO流之文件输入输出流

    Java中的IO流之文件输入输出流 1.文件流介绍 文件流是以字节为单位进行读写文件的,所以属于字节流,并且是低级流.文件流位于java.io包下. 输入输出流分别是FileInputSteam和Fi ...

  6. java输出流缓冲区内容清除,Java输入输出流与缓冲区的使用

    Java输入输出流与缓冲区的使用,有需要的朋友可以参考下. 一,Input/Output流: 将外设中的数据读取到内存中就是输入. 将内存中的数据写入到外设中就是出. I/O流就是用来处理设备间的 . ...

  7. java实验 输入输出流_java实验七 输入输出流

    有关java的实验和程序 实验七 输入输出流 一.实验目的和要求 目的: 1.掌握使用输入输出流进行文件的读写操作. 要求: 1.实验报告给出内容1,2的填充代码以及内容3的全部源代码. 二.实验内容 ...

  8. java实验七输入输出流_实验六_Java的输入输出流

    JAVA的输入输出流 实验六 Java的输入输出流 实验目的 1. 理解I/O流的概念,掌握其分类 2. 掌握文本文件读写.二进制文件读写 实验环境 JDK1.4以上版本, Eclipse集成开发环境 ...

  9. 【java开发系列】—— java输入输出流

    前言 任何语言输入输出流都是很重要的部分,比如从一个文件读入内容,进行分析,或者输出到另一个文件等等,都需要文件流的操作.这里简单介绍下reader,wirter,inputstream,output ...

最新文章

  1. 在python中print 应用_Python print正确使用方法浅析
  2. Hibernate学习4—关联关系一对多映射2
  3. MySQL 5.6--------SSL连接最佳实战
  4. java wcf 未提供用户名_WCF的用户名密码认证
  5. MySQL中的单引号
  6. 中如何计算工龄_在Substrate中如何计算交易权重
  7. 有驾照不等于会开车,教你开车技巧27招
  8. 软考信息安全工程师考试历年真题汇总及试题分布统计
  9. 新的log4j2.xml
  10. maven 分批打包_IDEA maven 多模块打包问题总结
  11. ICPC North Central NA Contest 2017 B - Pokemon Go Go
  12. Unity说明文档翻译-Time Manager
  13. 不登高山,不知天之高也;不临深溪,不知地之厚也
  14. 安卓打包:jks Invalid keystore format 报错解决
  15. 外服游戏服务器如何显示中文,避免国外服务器出现乱码的办法
  16. js如何往数组Array中添加删除元素
  17. 微信营销系统(第三方微信平台)之微分销模块拓展
  18. pdf转word ocr_OCR免费识别撞上PDF免费转WORD,这下尴尬了!
  19. 计算机组成与结构r形式,计算机组成与结构试卷
  20. 微软服务器操作系统软件价格,供应微软服务器操作系统软件

热门文章

  1. 计算机系统-CPU优化/特权级
  2. 大学计算机科学计术的总结,学好大学计算机科学与技术 要注意当天小结
  3. linux java 1.6 下载地址_linux 安装配置java环境 jdk1.6 jdk-6u45-linux-x64.bin
  4. linux中lsattr命令,在Linux中用chattr和lsattr命令管理文件和目录属性
  5. itunes备份包括哪些内容_建筑工程的招标包括哪些内容?
  6. 根据控制点坐标对完成坐标转换
  7. C、C++语言中参数的压栈顺序
  8. ARCGIS知乎上的好文章
  9. 回文日期(NOIP2016 普及组第二题)
  10. [POJ 1273]Drainage Ditches