【Go】-bufio库解读

目录

Reader和Writer接口

bufio.Reader/Writer

小结

其他函数-Peek、fill

Reader小结

Writer

Scanner结构体

缓冲区对于网络数据读写的重要性


Reader和Writer接口

        在net/http包生成的Conn 接口的实例中有两个方法叫做Read和Write接口

type Conn interface {Read(b []byte)(n int,err error)Write(b []byte)(n int,err error)Close() error...
}

        该接口实际上实现了io.Reader和io.Write的方法

type Reader interface {Read(p []byte)(n int, err error)
}type Writer interface {Write(p []byte)(n int, err error)
}

        在Unix中,一切皆文件,Golang 把这个思想贯彻到更远,因为本质上我们对文件的抽象就是一个可读可写的一个对象,也就是实现了io.Writer 和 io.Reader 的对象我们都可以称为文件。
其中我们需要注意Reader接口的几个细节:

  • Reader.Read函数最多读取len(p)字节的数据保存到P中,若n<len(p)则立即返回
  • 在输入流结束时,方法会返回n>0字节,但是error可能是EOF也可能是nil,在这种情况下再次调用read方法肯定会返回(0,EOF)
  • 调用read方法,当n>0要优先处理读入数据再处理err---即使我们在读取的时候遇到错误,但是也应该也处理已经读到的数据,因为这些已经读到的数据是正确的,如果不进行处理丢失的话,读到的数据就不完整了。

bufio.Reader/Writer

        那么我们对Socket读写的行为就可以巨象成对Conn的Read和Write。

        demo.go

func main() {str := strings.Repeat("123", 20)reader := strings.NewReader(str)fmt.Printf("the size of strings reader is %d\n", reader.Size())fmt.Println("create a new reader")var buffer *bufio.Reader = bufio.NewReader(reader)fmt.Printf("the default size of buffered reader is %d\n", buffer.Size())fmt.Printf("The number of unread bytes in the buffer: %d\n", buffer.Buffered())buf1 := make([]byte, 8)for {n, err := buffer.Read(buf1)fmt.Printf("read the number of Context is %d\n", n)fmt.Print("the context is")fmt.Println(string(buf1[:n]))if err == io.EOF {break}if err != nil {log.Fatal(err)}fmt.Printf("The number of unread bytes in the buffer: %d\n", buffer.Buffered())}n, err := buffer.Read(buf1)fmt.Println("try again!")fmt.Println(n, err)
}

        在该例子中
        1.创建了一个长度为60字节的字符串(因为数字对应的ASCII码长度为1字节),利用该字符串创建了 bufio.Reader为 buffer(default size为4096字节,而且初始化时当前缓冲区可读取的字节数为0)

        2.创建了一个长度为8的切片用于循环读取
        3.直到读取到EOF为止退出循环
        4.当尝试再次读取时失败,数据流被设计成单向(是不是意味着跟读取文件一样,内部有一个读指针表示已读且不重置?)

        我们将断点打在bufio.NewReader尝试通过源码解释这现象

dlv debug demo.go
b bufio.NewReader

        newReader实际上是对NewReaderSize的包装,默认缓冲区大小为defaultBufSize,defaultBufSize = 4096

                                                ​​​​​​​        ​​​​​​​        

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就是*bufio.Readerr := new(Reader)r.reset(make([]byte, size), rd)return r
}

        如果我们创建io.Reader时的字符串比4096还要长,那么我们直接返回那个io.Reader。当然缓冲区也有最小长度要求minReadBufferSize = 16。之后的new关键词我们也知道,底层调用mallocgc分配内存,reset函数是重置自身所有字段

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

        可以看到buf缓冲区实际上就是一个size长度的切片,我们回头再看下bufio.Reader结构体

// Reader implements buffering for an io.Reader object.
type Reader struct {buf          []byterd           io.Reader // reader provided by the clientr, w         int       // buf read and write positionserr          errorlastByte     int // last byte read for UnreadByte; -1 means invalidlastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
  • buf 知道了是缓冲区切片
  • rd 是我们在调用NewReader时传入的io.Reader,也就是说他底层保存着原始数据的拷贝,是一个底层读取器,那是不是意味着缓冲区要从这里面读取数据?同时也解释了为啥要返回一个指针,因为拷贝传值占用太大了
  • r和w 是代表缓冲区进行下一次读/写的开始,是已读/已写指针,看到这个我们就明白了如何被设计成单向数据流
  • lastByte/lastRuneSize 用于记录缓冲区最后一个被读的字节和码点,读回退时用到

        我们为了了解buf和rd的关系,继续给Read打断点

r
b bufio.Reader.Read

Read函数

func (b *Reader) Read(p []byte) (n int, err error) {
//首先确定被写入切片长度n = len(p)if n == 0 {if b.Buffered() > 0 {return 0, nil}return 0, b.readErr()}
//一开始肯定是0进入该分支if b.r == b.w {
//EOF时也会触发这个if b.err != nil {return 0, b.readErr()}
//当被写入切片长度远大于缓冲区
//直接从原始io.Reader整个读入if len(p) >= len(b.buf) {// Large read, empty buffer.// Read directly into p to avoid copy.n, b.err = b.rd.Read(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.
//r=w其中一种情况就是第一次读取
//从原始io.Reader中即rd读取buf
//之后写指针往前推移b.r = 0b.w = 0n, b.err = b.rd.Read(b.buf)if n < 0 {panic(errNegativeRead)}if n == 0 {return 0, b.readErr()}
//此时b.w = 60是io.Reader的数据最长长度
//也就是说下一次进入这里是读完的时候b.w += n}
// n 为传入的切片长度
//拷贝进切片后读指针往前推移n = copy(p, b.buf[b.r:b.w])
//b.r = 8b.r += nb.lastByte = int(b.buf[b.r-1])b.lastRuneSize = -1return n, nil

        buf确实是从rd中读取数据,利用w和r确保已读计数之前的字节都被读取且不会再次被读,因此在切片上(demo中指buf1)是安全的,我们之前的buffer.Buffered的读取缓冲区实际上是w-r的差值,即buf中数据的长度减去已读的,刚开始两者都是0,所以一开始为0

        ​​​​​​​        


小结

        


其他函数-Peek、fill

        有个奇怪的地方就是Peek方法用于查看未读取数据的n个字节,但并不会改变bufio.Reader的状态

func (b *Reader) Peek(n int) ([]byte, error) {if n < 0 {return nil, ErrNegativeCount}
//设置为-1表示回退操作失败,即不能执行回退b.lastByte = -1b.lastRuneSize = -1
//未读取数据的长度小于n 且缓冲区未满
//执行fill填充缓冲区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 大于缓冲区长度就直接返回全部及错误提示if n > len(b.buf) {return b.buf[b.r:b.w], ErrBufferFull}// 0 <= n <= len(b.buf)
//有效数据长度小于n,表示fill没填满缓冲区,
//此时最多返回所有有效数据并产生错误提示var err errorif avail := b.w - b.r; avail < n {// not enough data in buffern = availerr = b.readErr()if err == nil {err = ErrBufferFull}}return b.buf[b.r : b.r+n], err
}

        这个函数有一个问题就是return的时候直接返回切片,那意味着调用者可以直接修改缓冲区的值!造成数据泄露风险,当下次读取时r和w改变,当前数据位置也改变,慎用该函数
同理我们阅读文档发现ReadLine和ReadSlice有同样的返回类型

ReadLine() (line []byte,isPrefix bool,err error)
ReadSlice() (line []byte,err error)
ReadSlice
ReadLine

        两者都会返回缓存切片,都存在内存泄漏问题

        fill函数

func (b *Reader) fill() {// Slide existing data to beginning.
//b.r>0表示已经读了一部分想要从0开始读就需要重置
//这里直接修改了缓冲区数据,进行数据平移if b.r > 0 {copy(b.buf, b.buf[b.r:b.w])b.w -= b.rb.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 =100
//for i := maxConsecutiveEmptyReads; i > 0; i-- {
//从rd读取w位置数据到缓冲区n, err := b.rd.Read(b.buf[b.w:])if n < 0 {panic(errNegativeRead)}b.w += nif err != nil {b.err = errreturn}if n > 0 {return}}b.err = io.ErrNoProgress
}

        fill先查已读计数,大于0时有两种情况:

  • 1.当有效数据长度大于无效数据长度(r已读的数据),copy可以直接覆盖
  • 2.当有效数据长度小于无效数据长度,即不能完全用copy覆盖怎么办?无所谓反正是根据[b.r:b.w]来读取,后面舍弃就行,这就体现了r和w设计的妙处
无效数据过长情况

        fill方法只要在开始时发现其所属值的已读计数大于0,就会对缓冲区进行一次压缩。之后,如果缓冲区中还有可写的位置,那么该方法就会对其进行填充。
        在填充缓冲区的时候,fill方法会试图从底层读取器那里,读取足够多的字节,并尽量把从已写计数w代表的索引位置到缓冲区末尾之间的空间都填满


Reader小结

        Reader内部的缓冲区buf实际上是一个切片默认大小为4KB,它介于底层读取器rd和调用方之间,Reader渎职一般是先从底层读取器rd中读一部分数据(缓冲区足够大就全放入)放入缓冲区,再从buf中读取,并且从安全性方面考虑,Peek、ReadSlice和ReadLine方法都会造成内存泄漏问题,调用方可以直接修改缓冲区,这十分危险


Writer

func main() {str := strings.Repeat("hi", 100)basicWriter := &strings.Builder{}fmt.Printf("New a buffered writer writer size \n")writer1 := bufio.NewWriter(basicWriter)fmt.Println()fmt.Printf("the number of buffered bytes %d\n", writer1.Buffered())fmt.Printf("the number of unused bytes in the buffer:%d\n", writer1.Available())begin, end := 0, 40fmt.Printf("Write %d byte into the writer\n", end-begin)writer1.Write(([]byte(str))[begin:end])fmt.Printf("the number of buffered bytes %d\n", writer1.Buffered())fmt.Printf("the number of unused bytes in the buffer:%d\n", writer1.Available())fmt.Println()writer1.Flush()fmt.Printf("the number of buffered bytes %d\n", writer1.Buffered())fmt.Printf("the number of unused bytes in the buffer:%d\n", writer1.Available())}New a buffered writer writer size the number of buffered bytes 0
the number of unused bytes in the buffer:4096
Write 40 byte into the writer
the number of buffered bytes 40
the number of unused bytes in the buffer:4056the number of buffered bytes 0
the number of unused bytes in the buffer:4096

        NewWriter和NewReader一样,默认给一个4k缓冲区

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,}
}type Writer struct {err errorbuf []byten   intwr  io.Writer
}

        Writer结构体字段没那么多:

  • err存储报错信息
  • buf 缓冲区
  • n 已写指针,对缓冲区进行下一次写入的开始索引
  • wr 底层写入器

函数方法的思想和Reader类似,这里不再赘述,需要注意的是Flush方法将缓冲区buf推入wr

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])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 -= nb.err = errreturn err}b.n = 0return nil
}

        实际上是通过n进行的覆盖,这个方法在Write中也出现过                        
 

        为的是给后续新数据腾出空间,如果Write方法发现需要写入的字节太多,同时缓冲区已空,那么它就会跨过缓冲区,并直接把这些数据写到底层写入器中。
        总之,在通常情况下,只要缓冲区中的可写空间无法容纳需要写入的新数据,Flush方法就一定会被调用。不过,在你把所有的数据都写入Writer值之后,再调用一下它的Flush方法,显然是最稳妥的。


Scanner结构体

        从上文的分析中我们了解到读取一行ReadLine底层实际是通过ReadSlice('\n')实现,存在内存泄漏的问题。因此对于简单的读取一行,Reader中没有让人特别满意的方法,于是1.1增加了Scanner类型
简单demo

func main() {scanner := bufio.NewScanner(os.Stdin)for scanner.Scan() {fmt.Println(scanner.Text())}if err := scanner.Err(); err != nil {fmt.Fprintln(os.Stderr, "reading standard input:", err)}
}

        Scanner结构体(scan.go)

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和buf是底层读取和缓冲,start和end大概率猜到是已读已写指针
  • split、maxTokenSize、token需要往下看一下

split类型签名如下

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

其中ScanBytes/Line/Runes/Words是其的具体实现

NewScanner

        默认使用ScanLines,且初始化时不创建缓冲区,maxTokenSize大小默认为64KB
实际上SplitFunc定义了对输入进行分词的slit签名,data是未处理数据,atEOF表示是否还有数据(EOF才算结尾),advance表示读取字节数,token表示下一个结果数据

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

ScanLine源码

func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
//先判断是否在EOFif atEOF && len(data) == 0 {return 0, nil, nil}
//判断有没有换行标志‘\n’,没有indexByte返回-1
//drop默认去掉结尾的'\r'if i := bytes.IndexByte(data, '\n'); i >= 0 {// We have a full newline-terminated line.return i + 1, dropCR(data[0:i]), nil}// If we're at EOF, we have a final, non-terminated line. Return it.if atEOF {return len(data), dropCR(data), nil}// Request more data.return 0, nil, nil
}func dropCR(data []byte) []byte {if len(data) > 0 && data[len(data)-1] == '\r' {return data[0 : len(data)-1]}return data
}

        split会根据传入的函数类型截取数据流(Line就是截取直到'\n',Byte就是截取一个,‘Words’截取直到'\t\n\v\f\r',因为这几个都是空格unicode.IsSpace(),Rune截取码点),因此splite是一个分词策略,token就是这个分词策略所分出的词
我们可以使用Scanner.Split方法来更换分词策略

func (s *Scanner) Split(split SplitFunc) {if s.scanCalled {panic("Split called after Scan")}s.split = split
}

Scanner.Scan方法内部会循环直到获得一个分词
        伪代码:

func Sca(){for {advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)s.token = tokenif token!=nil {return true}}
}

        内部调用split方法存储结果在tokn中,当token=nil会移动已读已写指针Scanner.Text

func (s *Scanner) Text() string {return string(s.token)
}

        因为每执行一次Scan,token会被覆盖一次,因此需要使用for循环读取(注意buf缓冲区在Scan调用split的时有使用到)


缓冲区对于网络数据读写的重要性

        缓冲区对于接收方的意义在于减少程序压力,不至于被一次性的大量数据给压垮。对于发送方的意义就是节省带宽,一次性发送更多数据,也减少了用户态到内核态的切换(内核协议栈不确定用户一次要发多少数据,如果用户来一次就发一次,如果数据多还好说,如果少了,那网络I/O很频繁,而真正发送出去的数据也不多,所以为了减少网络I/O使用了缓存的策略。)
缓冲区什么时候发送由内核决定,缓冲区也不能无线大,因为用户缓冲区发送到内核缓冲区再发送到网卡,网卡一次发出去的数据有最大长度,不管累计多少最后还是分片发送,这样缓冲区太大没有意义,数据传输也是有延时要求的,不能总在缓冲区呆着,太大也浪费资源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/473533.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

场景营销在企业定制开发 AI 智能名片 S2B2C 商城小程序中的应用与价值

摘要&#xff1a;本文深入剖析了品牌广告效果不佳与场景营销缺失之间的内在联系&#xff0c;阐述了场景营销对于品牌落地和转化的关键意义。同时&#xff0c;详细探讨了如何将场景营销理念与实践应用于企业定制开发的 AI 智能名片 S2B2C 商城小程序中&#xff0c;借助移动时代的…

uniapp 实现tabbar分类导航及滚动联动效果

思路&#xff1a;使用两个scroll-view&#xff0c;tabbar分类导航使用scrollleft移动&#xff0c;内容联动使用页面滚动onPageScroll监听滚动高度 效果图 <template><view class"content" ><view :class"[isSticky ? tab-sticky: ]">…

Flutter中的Material Theme完全指南:从入门到实战

Flutter作为一款热门的跨平台开发框架&#xff0c;其UI组件库Material Design深受开发者喜爱。本文将深入探讨Flutter Material Theme的使用&#xff0c;包括如何借助Material Theme Builder创建符合产品需求的主题风格。通过多个场景和代码实例&#xff0c;让你轻松掌握这一工…

aws中AcmClient.describeCertificate返回值中没有ResourceRecord

我有一个需求&#xff0c;就是让用户自己把自己的域名绑定我们的提供的AWS服务器。 AWS需要验证证书 上一篇文章中我用php的AcmClient中的requestCertificate方法申请到了证书。 $acmClient new AcmClient([region > us-east-1,version > 2015-12-08,credentials>[/…

Oracle 19c PDB克隆后出现Warning: PDB altered with errors受限模式处理

在进行一次19c PDB克隆过程中&#xff0c;发现克隆结束&#xff0c;在打开后出现了报错&#xff0c;PDB变成受限模式&#xff0c;以下是分析处理过程 09:25:48 SQL> alter pluggable database test1113 open instancesall; Warning: PDB altered with errors. Elapsed: 0…

【3D Slicer】的小白入门使用指南九

定量医学影像临床研究与实践 任务 定量成像教程 定量成像是从医学影像中提取定量测量的过程。 本教程基于两个定量成像的例子构建: - 形态学:缓慢生长肿瘤中的小体积变化 - 功能:鳞状细胞癌中的代谢活动 第1部分:使用变化跟踪模块测量脑膜瘤的小体积变化第2部分:使用PET标…

二、神经网络基础与搭建

神经网络基础 前言一、神经网络1.1 基本概念1.2 工作原理 二、激活函数2.1 sigmoid激活函数2.1.1 公式2.1.2 注意事项 2.2 tanh激活函数2.2.1 公式2.2.2 注意事项 2.3 ReLU激活函数2.3.1 公式2.3.2 注意事项 2.4 SoftMax激活函数2.4.1 公式2.4.2 Softmax的性质2.4.3 Softmax的应…

VMWare虚拟机安装华为欧拉系统

记录一下安装步骤&#xff1a; 1.在vmware中创建一个新的虚拟机&#xff0c;步骤和创建centos差不多 2.启动系统 具体的看下图&#xff1a; 启动虚拟机 耐心等待 等待进度条走完重启系统就完成了

如何进入python交互界面

Python交互模式有两种&#xff1a;图形化的交互模式或者命令行的交互模式。 打开步骤&#xff1a; 首先点击开始菜单。 然后在搜索栏中输入Python&#xff0c;即可看到图形化的交互模式&#xff08;IDLE&#xff08;Python 3.7 64-bit&#xff09;&#xff09;与命令行的交互…

NVR录像机汇聚管理EasyNVR多品牌NVR管理工具视频汇聚技术在智慧安防监控中的应用与优势

随着信息技术的快速发展和数字化时代的到来&#xff0c;安防监控领域也在不断进行技术创新和突破。NVR管理平台EasyNVR作为视频汇聚技术的领先者&#xff0c;凭借其强大的视频处理、汇聚与融合能力&#xff0c;展现出了在安防监控领域巨大的应用潜力和价值。本文将详细介绍Easy…

【STM32】USB 简要驱动软件架构图

STM32 USB 软件架构比较复杂&#xff0c;建议去看 UM 1734 或者 st wiki STM32 USB call graph STM32 USB Device Library files organization Reference [1]: https://wiki.stmicroelectronics.cn/stm32mcu/wiki/Introduction_to_USB_with_STM32 [2]: UM1734

高翔【自动驾驶与机器人中的SLAM技术】学习笔记(十三)图优化SLAM的本质

一、直白解释slam与图优化的结合 我从b站上学习理解的这个概念。 视频的大概位置是1个小时以后&#xff0c;在第75min到80min之间。图优化SLAM是怎么一回事。 slam本身是有运动方程的&#xff0c;也就是运动状态递推方程&#xff0c;也就是预测过程。通过t1时刻&#xff0c…

Vue2教程002:Vue指令

文章目录 2、Vue指令2.1 开发者工具2.2 v-html2.3 v-show和v-if2.4 v-else和v-else-if2.5 v-on2.5.1 内联语句2.5.2 methods 2、Vue指令 2.1 开发者工具 通过谷歌应用商店安装&#xff08;需要科学上网&#xff09;通过极简插件安装 2.2 v-html Vue会根据不同的指令&#x…

使用WebSocket技术实现Web应用中的实时数据更新

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用WebSocket技术实现Web应用中的实时数据更新 使用WebSocket技术实现Web应用中的实时数据更新 使用WebSocket技术实现Web应用中…

单片机学习笔记 1. 点亮一个LED灯

把基础的东西都过一下&#xff0c;用来学习记录一下。 目录 1、Keil工程 2、Keil实现代码 3、烧录程序 0、实现的功能 点亮一个LED灯 1、Keil工程 打开Keil&#xff0c;Project----New uVision Project&#xff0c;工程文件命名----OK 选择单片机类型AT89C52&#xff0c;和…

使用Web Animations API实现复杂的网页动画效果

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂…

计算机组成与原理(2) basic of computer architecture

Instruction Set Architecture (ISA) 和 Hardware System Architecture (HSA) 是计算机体系结构中两个重要的层次&#xff0c;它们各自的职责和作用如下&#xff1a; Instruction Set Architecture (ISA) 定义 ISA是指令集体系结构&#xff0c;是硬件和软件之间的接口。它定义…

Python Excel XLS或XLSX转PDF详解:七大实用转换设置

目录 使用工具 Python将Excel文件转换为PDF Python将Excel文件转换为带页码的PDF Python将Excel文件转换为特定页面尺寸的PDF Python将Excel文件转换为PDF并将内容适应到一页 Python将Excel文件转换为PDF/A Python将Excel文件中的工作表转换为单独的PDF Python将Excel工…

【C++】红黑树封装map—set

1 .关联式容器 C中的map是标准模板库&#xff08;STL&#xff09;中的一种关联容器&#xff0c;它存储的是键值对&#xff08;key-value pairs&#xff09;&#xff0c;其中每个键都是唯一的。 键值对&#xff1a; 用来表示具有一一对应关系的一种结构&#xff0c;该结构中一…

系统思考—结构影响行为

最近在和一些企业领导者交流时&#xff0c;发现一个共性——创始人都非常厉害&#xff01;战略清晰、点子多、方向准&#xff0c;简直就是企业的“定海神针”。但往往问题在了下一层级&#xff1a;如何把创始人的智慧传承下去&#xff0c;甚至复制到团队里&#xff0c;这成了一…