Hello World - 2020年4月
type: channel
link: http://47.76.222.76/index.php/2020/04
description: Just So So ...
items: Go 调度器
items: Go 垃圾回收
items: Go 栈管理
items: Go 内存分配
items: Go 序列化
items: Go 文件编程
items: Go nil

Go 调度器
type: item
date: 2020-04-29T08:23:00+00:00
description: POSIX线程API（实现了这个api的库常被称作pthreads）在很大程度上可以看做是对现有Unix进程模型的逻辑延伸，线程和进程有很多相似处。线程有自己的信号掩码，可以分配与CPU关联，可以被放入cgroups，可以被查询使用了哪些资源。所有这些控制的特性都增加了开销，当你的程序中有10万个线程的时候，这些开销将会非常巨大。另一个问题是，操作系统在Go模型下不能做出好的调度决策。举例来说，Go垃圾回收器在执行一次回收时，需要所有线程都停止，使得内存处于一致性状态。这导致需要等待所有运行着的线程到达某一个我们可以确定内存一致的时刻。当你有很多线程被调度运行时，你必须等待它们达到一致性状态。而Go调度器可以决策出只调度到它可以确认内存是一致的时刻即可。这意味着当我们停下来做垃圾回收时，我们只需要等待那些正在CPU上运行的活跃的线程。Go GC的时候需要全局内存一致性，也即全局都停下来不操作内存，Go的调度器更容易做到这一点。比如所有空闲系统线程不用管，所有运行时的系统线程都在执行完当前协程后暂停。Go语言在并发编程方面有强大的能力，通过协程（Goroutine）实现，由调度器来管理执行。与操作系统提供的线程相比，去掉无用特性，使用一个小的可伸缩的栈，更短的上下文切换，显得十分轻量。调度器通过使用与CPU数量相等的线程减少线程频繁切换的内存开销，同时在每一个线程上执行额外开销更低的Goroutine来降低操作系统和硬件的负载。一、Go调度器今天的Go语言调度器有着优异的性能，最初的调度器不仅实现非常简陋，也无法支撑高并发的服务，调度器经过几个大版本的迭代才有今天的优异性能，几个不同版本的调度器引入了不同的改进，也存在不同的缺陷。单线程调度器 0.x只包含40多行代码；程序中只能存在一个活跃线程，由G-M模型组成；多线程调度器 1.0允许运行多线程的程序；全局锁导致竞争严重；任务窃取调度器 1.1引入了处理器P，构成了目前的G-M-P模型；在处理器P的基础上实现了基于工作窃取的调度器；在某些情况下，Goroutine不会让出线程，进而造成饥饿问题；时间过长的垃圾回收（Stop-the-world，STW）会导致程序长时间无法工作；抢占式调度器 1.2~至今基于协作的抢占式调度器 1.2~1.13通过编译器在函数调用时插入抢占检查指令，在函数调用时检查当前Goroutine是否发起了抢占请求，实现基于协作的抢占式调度；Goroutine可能会因为垃圾回收和循环长时间占用资源导致程序暂停；基于信号的抢占式调度器 1.14~至今实现基于信号的真抢占式调度；垃圾回收在扫描栈时会触发抢占调度；抢占的时间点不够多，还不能覆盖全部的边缘情况；非均匀存储访问调度器 提案对运行时的各种资源进行分区；实现非常复杂，到今天还没有提上日程1、单线程调度器0.x版本调度器只包含表示Goroutine的G和表示线程的M两种结构，全局也只有一个线程。在这时Go语言的调度器还是由C语言实现的，调度函数runtime.schedule也只包含40多行代码。该函数会遵循如下的过程调度Goroutine：获取调度器的全局锁；调用runtime.gosave保存栈寄存器和程序计数器；调用runtime.nextgandunlock获取下一个需要运行的Goroutine并解锁调度器；修改全局线程m上要执行的Goroutine；调用runtime.gogo函数运行最新的Goroutine；显然这个单线程调度器的唯一优点就是能运行。2、多线程调度器Go语言在1.0版本正式发布时就支持了多线程的调度器，与上一个版本几乎不可用的调度器相比，Go语言团队在这一阶段实现了从不可用到可用的跨越。多线程版本的调度函数runtime.schedule包含70多行代码。整体的逻辑与单线程调度器没有太多区别，因为我们的程序中可能同时存在多个活跃线程，所以多线程调度器引入了GOMAXPROCS变量帮助我们灵活控制程序中的最大处理器数，即活跃线程数。多线程调度器的主要问题是调度时的锁竞争会严重浪费资源。该调度器有以下问题需要解决：调度器和锁是全局资源，所有的调度状态都是中心化存储的，锁竞争问严重；线程需要经常互相传递可运行的Goroutine，引入了大量的延迟；每个线程都需要处理内存缓存，导致大量的内存占用并影响数据局部性（Data locality）；系统调用频繁阻塞和解除阻塞正在运行的线程，增加了额外开销；3、任务窃取调度器在现有多线程调度器基础上提出了两个改进的手段：在当前的G-M模型中引入了处理器P，增加中间层；在处理器P的基础上实现基于工作窃取的调度器；使用了新的G-M-P模型，调度器的runtime.schedule函数在这个版本的调度器中反而更简单了：static void sch…

Go 垃圾回收
type: item
date: 2020-04-27T08:38:00+00:00
description: 编程语言通常会使用手动和自动两种方式管理内存，C、C++ 以及Rust等编程语言使用手动的方式管理内存，工程师需要主动申请或者释放内存；而Python、Ruby、Java和Go等语言使用自动的垃圾回收（Garbage Collection）机制，简称GC，GC会自动检测程序运行过程中的垃圾并回收它。优点：自动回收内存，避免忘记释放内存空间而发生的内存泄露和“悬垂指针”导致严重的无法预期的BUG。程序员就不用再去担心因为忘了释放内存，从而大大减轻了负担。也不用再去头疼费事的内存管理，让程序员告别恼人的内存管理，把精力集中在更本质的编程工作上。缺点：程序运行垃圾收集回收系统总是消耗一定的资源，不管怎么优化，对程序性能还是有一定的影响，包括：暂停程序、额外的存储开销、额外的计算开销、内存读写速度下降等等。一、基本算法1960年首次发布了GC算法，直到现在为止，垃圾回收算法都是基于以下三种算法的组合或应用。1、标记清除标记清除算法由标记阶段和清除阶段构成。标记阶段是把所有活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象，也就是非活动对象回收的阶段，通过这两个阶段，就可以令不能利用的内存空间重新得到利用。标记阶段需要暂停程序（Stop the world，STW），扫描所有的对象，如果对象很多，暂停程序时间就很长，但是实现简单，搭配其他一些算法可以实现高效的GC算法。标记阶段：从根对象（全局对象和栈对象）出发查找并标记堆中所有存活的对象；清除阶段：遍历堆中的全部对象，回收未被标记的垃圾对象并将回收的内存加入空闲链表；2、引用计数给每个对象添加一个引用计数属性，新增一个引用时计数加1，引用释放时计数减1，当被引用数的值为0时，对象马上就会把自己作为空闲空间连接到空闲链表。也就是说，各个对象在变成垃圾的同时就会立刻被回收。内存管理的开销分布于整个应用程序运行期间，无需挂起应用程序的运行来做垃圾回收。虽然优点佷明显，但是局限性很多，包括：频繁的更新计数，每一个对象的操作都可能增加或减少计数，然而程序中对象操作很多计数器需要占用很多位，如果是64位系统，地址数量即最大的对象数量是2^64，就需要一个64位的存储空间算法简单但是实现复杂，每一个对象都要插入计数器，且无法处理循环引用3、复制算法只把某个空间里的活动对象复制到其他空间，把原空间里的所有对象都回收掉。复制活动对象的原空间称为From空间，将粘贴活动对象的新空间称为To空间。GC复制算法是利用From空间进行分配的。当From空间被完全占满时，GC会将活动对象全部复制到To空间。当复制完成后，该算法会把From空间和To空间互换，GC也就结束了。From空间和To空间大小必须一致。这是为了保证能把From空间中的所有活动对象都收纳到To空间里。可实现内存高速分配且不会产生碎片化，但只能利用一半的内存空间，如果使用递归调用的方式复制对象则可能存在栈溢出的情况，迭代复制更高效而且不存在栈溢出，只是需要一点额外的内存作为迭代缓存。二、优化算法传统的垃圾收集算法会在垃圾收集的执行期间暂停应用程序，一旦触发垃圾收集，垃圾收集器就会抢占CPU的使用权占据大量的计算资源以完成标记和清除工作，然而很多追求实时的应用程序无法接受长时间的STW。垃圾收集器一旦开始执行就会浪费大量的计算资源，为了减少应用程序暂停的最长时间和垃圾收集的总暂停时间，我们会使用下面的策略优化现代的垃圾收集器：增量垃圾收集：增量地标记和清除垃圾，降低应用程序暂停的最长时间；并发垃圾收集：利用多核的计算资源，在用户程序执行时并发标记和清除垃圾；因为增量和并发两种方式都可以与用户程序交替运行，所以需要使用屏障技术保证垃圾收集的正确性；与此同时，应用程序也不能等到内存溢出时触发垃圾收集，因为当内存不足时，应用程序已经无法分配内存，这与直接暂停程序没有什么区别，增量和并发的垃圾收集需要提前触发并在内存不足前完成整个循环，避免程序的长时间暂停。增量式（Incremental）的垃圾收集是减少程序最长暂停时间的一种方案，它可以将原本时间较长的暂停时间切分成多个更小的GC时间片，虽然从垃圾收集开始到结束的时间更长了，但是这也减少了应用程序暂停的最大时间。增量式的垃圾收集需要与三色标记法一起使用，为了保证垃圾收集的正确性，我们需要在垃圾收集开始前打开写屏障，这样用户程序对内存的修改都会先经过写屏障的处理，保证了堆内存中对象关系的强三色不变性或者弱三色不变性。虽然增量式的垃圾收集能够减少最大的程序暂停时间，但是增量式收集也会增加一次GC循环的总时间。并发式（Concurrent）的垃圾收集不仅能够减少程序的最长暂停时间，还能减少整个垃圾收集阶段的时间，通过开启读写屏障、利用多核优势与用户程序并行执行，并发垃圾收集器确实能够减少垃圾收集对应用程…

Go 栈管理
type: item
date: 2020-04-26T06:53:00+00:00
description: 应用程序的内存一般会分成堆区和栈区两个部分。栈区的内存一般由编译器自动进行分配和释放，其中存储着函数的入参以及局部变量，这些参数会随着函数的创建而创建，函数的返回而消亡，一般不会在程序中长期存在，这种线性的内存分配策略有着极高地效率，但是工程师也往往不能控制栈内存的分配，这部分工作基本都是由编译器自动完成的。一、线程栈寄存器是中央处理器（CPU）中的稀缺资源，它的存储能力非常有限，但是能提供最快的读写速度，充分利用寄存器的效率可以构建高性能的应用程序。寄存器在物理机上非常有限，然而栈区的操作就会使用到两个以上的寄存器，这足以说明栈内存在应用程序的重要性。栈寄存器在是 CPU 寄存器中的一种，它的主要作用是跟踪函数的调用栈，Go 语言的汇编代码中包含 BP 和 SP 两个栈寄存器，它们分别存储了栈的基址指针和栈顶的地址，栈内存与函数调用的关系非常紧密。由于历史的设计问题，目前的栈区内存都是从高地址向低地址扩展的，当应用程序申请或者释放栈内存时只需要修改 SP 寄存器的值，这种线性的内存分配方式与堆内存相比更加快速，占用极少的额外开销。如果我们在Linux操作系统中执行pthread_create系统调用，进程会启动一个新的线程，如果用户没有通过软资源限制RLIMIT_STACK指定线程栈的大小，那么操作系统会根据架构选择不同的默认栈大小。多数架构上默认栈大小都在2 ~ 4MB左右，极少数架构会使用32MB作为默认大小，用户程序可以在分配的栈上存储函数参数和局部变量。然而这个固定的栈大小在某些场景下可能不是一个合适的值，如果一个程序需要同时运行几百个甚至上千个线程，那么这些线程中的绝大部分都只会用到很少的栈空间，而如果函数的调用栈非常深，固定的栈大小也无法满足用户程序的需求。线程和进程都是代码执行的上下文（Context of Execution），但是如果一个应用程序中包含成百上千个执行上下文并且每个上下文都是线程，就会占用大量的内存空间并带来其他的额外开销，Go语言在设计时认为执行上下文应该是轻量级的，所以在它实现了用户级的Goroutine作为执行上下文。二、逃逸分析在 C 语言和 C++ 这类需要手动管理内存的编程语言中，将对象或者结构体分配到栈上或者堆上是由工程师自主决定的，这也为工程师的工作带来的挑战，如果工程师能够精准地为每一个变量分配最合理的空间，那么整个程序的运行效率和内存使用效率一定是最高的，但是手动分配内存会导致如下的两个问题：不需要分配到堆上的对象分配到了堆上，浪费内存空间；需要分配到堆上的对象分配到了栈上，悬挂指针、影响内存安全；与悬挂指针相比，浪费的内存空间反而是小问题。如果一个该分配到堆上的变量分配到栈上，函数调用返回时这个局部变量会被回收，调用方获取的是危险的悬挂指针，我们不确定当前指针指向的值是否合法，这种问题在大型项目中是比较难以发现和定位的。int *dangling_pointer() { int i = 2; return &i; } 在编译器优化中，逃逸分析（Escape analysis）是用来决定指针动态作用域的方法。Go语言的编译器使用逃逸分析决定哪些变量应该在栈上分配，哪些变量应该在堆上分配，其中包括使用new、make和字面量等方法隐式分配的内存，Go语言的逃逸分析遵循以下两个不变性：指向栈对象的指针不能存在于堆中；指向栈对象的指针不能在栈对象回收后存活；逃逸分析是静态分析的一种，在编译器解析了Go语言源文件后，它可以获得整个程序的抽象语法树（Abstract syntax tree，AST），编译器可以根据抽象语法树分析静态的数据流，通过以下几个步骤实现静态分析的全过程（src/cmd/compile/internal/gc/escape.go）：构建带权重的有向图，其中顶点表示被分配的变量，边表示变量之间的分配关系，权重表示寻址和取地址的次数；遍历对象分配图并查找违反两条不变性的变量分配关系，如果堆上的变量指向了栈上的变量，那么栈上的变量就需要分配在堆上；记录从函数的调用参数到堆以及返回值的数据流，增强函数参数的逃逸分析；决定变量是在栈上还是堆上虽然重要，但是这是一个定义相对清晰的问题，我们可以通过编译器在统一作出决策。为了保证内存的绝对安全，编译器可能会将一些变量错误地分配到堆上，但是因为这些对也会被垃圾收集器处理，所以不会造成内存泄露以及悬挂指针等安全问题，解放了工程师的生产力。三、栈内存空间Go语言使用用户态线程Goroutine作为执行上下文，它的额外开销和默认栈大小都比线程小很多，然而Goroutine的栈内存空间和栈结构也在早期几个版本中发生过一些变化：v1.0 ~ v1.1：最小栈内存空间为4KB；v1.2：将最小栈内存提升到了8KB；v1.3：使用连续栈替换…

Go 内存分配
type: item
date: 2020-04-23T10:26:00+00:00
description: 程序中的数据和变量都会被分配到程序所在的虚拟内存中，内存空间包含两个重要区域栈区（Stack）和堆区（Heap）。函数调用的参数、返回值以及局部变量大都会被分配到栈上，这部分内存会由编译器进行管理；不同编程语言使用不同的方法管理堆区的内存，C++等编程语言会由工程师主动申请和释放内存，Go等编程语言会由工程师和编译器共同管理，堆中的对象由内存分配器分配并由垃圾收集器回收。Go语言程序内置运行时，内存分配器是运行时的一个组件。一、分配方法编程语言的内存分配器一般包含两种分配方法，一种是线性分配器（Sequential Allocator，Bump Allocator），另一种是空闲链表分配器（Free-List Allocator）。1、线性分配器线性分配（Bump Allocator）是一种高效的内存分配方法，但是有较大的局限性。当我们在编程语言中使用线性分配器，我们只需要在内存中维护一个指向内存特定位置的指针，当用户程序申请内存时，分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置。根据线性分配器的原理，我们可以推测它有较快的执行速度，以及较低的实现复杂度；但是线性分配器无法在内存被释放时重用内存。正是因为线性分配器的这种特性，我们需要合适的垃圾回收算法配合使用。标记压缩（Mark-Compact）、复制回收（Copying GC）和分代回收（Generational GC）等算法可以通过拷贝的方式整理存活对象的碎片，将空闲内存定期合并，这样就能利用线性分配器的效率提升内存分配器的性能了。因为线性分配器的使用需要配合具有拷贝特性的垃圾回收算法，所以C和C++等需要直接对外暴露指针的语言就无法使用该策略。2、空闲链表分配器空闲链表分配器（Free-List Allocator）可以重用已经被释放的内存，它在内部会维护一个类似链表的数据结构。当用户程序申请内存时，空闲链表分配器会依次遍历空闲的内存块，找到足够大的内存，然后申请新的资源并修改链表。因为不同的内存块以链表的方式连接，所以使用这种方式分配内存的分配器可以重新利用回收的资源，但是因为分配内存时需要遍历链表，所以它的时间复杂度就是O(n)。空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择，最常见的就是以下四种方式：首次适应（First-Fit）：从链表头开始遍历，选择第一个大小大于申请内存的内存块；循环首次适应（Next-Fit）：从上次遍历的结束位置开始遍历，选择第一个大小大于申请内存的内存块；最优适应（Best-Fit）：从链表头遍历整个链表，选择最合适的内存块；隔离适应（Segregated-Fit）：将内存分割成多个链表，每个链表中的内存块大小相同，申请内存时先找到满足条件的链表，再从链表中选择合适的内存块；Go语言采用的是隔离适应的内存分配策略，将内存分割成由4、8、16、32字节的内存块组成的链表，当我们向内存分配器申请8字节的内存时，会在8字节内存块的链表找到空闲的内存块并返回。隔离适应的分配策略减少了需要遍历的内存块数量，提高了内存分配的效率。二、分级分配线程缓存分配（Thread-Caching Malloc，TCMalloc）是用于分配内存的的机制，它比glibc中的malloc函数还要快很多。Go 语言的内存分配器就借鉴了TCMalloc的设计实现高速的内存分配，它的核心理念是使用多级缓存根据将对象根据大小分类，并按照类别实施不同的分配策略。Go语言会根据申请分配的内存大小选择不同的处理逻辑，将对象分成微对象（0, 16B）、小对象[16B, 32KB]和大对象（32KB, +∞）三种。因为程序中的绝大多数对象的大小都在32KB以下，而申请的内存大小影响Go语言运行时分配内存的过程和开销，所以分别处理大对象和小对象有利于提高内存分配器的性能。内存分配器不仅会区别对待大小不同的对象，还会将内存分成不同的级别分别管理，Go内存分配器引入线程缓存（Thread Cache）、中心缓存（Central Cache）和页堆（Page Heap）三个组件分级管理内存。线程缓存属于每一个独立的线程，它能够满足线程上绝大多数的内存分配需求，因为不涉及多线程，所以也不需要使用互斥锁来保护内存，这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时，就会使用中心缓存作为补充解决小对象的内存分配问题；在遇到32KB以上的对象时，内存分配器就会选择页堆直接分配大量的内存。这种多层级的内存分配设计与计算机操作系统中的多级缓存也有些类似，因为多数的对象都是小对象，我们可以通过线程缓存和中心缓存提供足够的内存空间，发现资源不足时就从上一级组件中获取更多的内存资源。注：go语言是没有线程的，这里的线程指的是go语言的协程。三、内存划分在Go语言1…

Go 序列化
type: item
date: 2020-04-09T15:44:00+00:00
description: 应用程序的交互，即数据的交互，永远离不开序列化，常见的数据交互格式是json。一、Go语言中的序列化和反序列化go语言中序列化对比：方式优点缺点binary性能高不支持不确定大小类型 int、slice、stringprotobuf支持多种类型，性能高需要单独存放结构，如果结构变动需要重新生成 .pb.go 文件gob支持多种类型性能低json支持多种类型性能低于 binary 和 protobufxml支持多种类型性能低于 json二、jsonjson格式数据是现在数据交互用的最多的数据格式，go一般通过json.Marshal()进行序列化，通过json.Unmarshal()反序列化。type Message struct { Id uint64 `json:"id"` Size uint64 `json:"size"` Data string `json:"data"` } func JsonEncodeDecode() { m1 := Message{3, 1024, "json"} var buf []byte var err error if buf, err = json.Marshal(m1); err != nil { log.Fatal("json marshal error:", err) } var m2 Message if err = json.Unmarshal(buf, &m2); err != nil { log.Fatal("json unmarshal error:", err) } }三、GobGob（Go binary 的缩写）是Go语言自己以二进制形式序列化和反序列化程序数据的格式，其方法在encoding中，只能在go程序之间进行数据交互。type Message struct { Id uint64 Size uint64 Data string } func GobEncodeDecode() { m1 := Message{2, 1024, "gob"} var buf bytes.Buffer enc := gob.NewEncoder(&buf) dec := gob.NewDecoder(&buf) if err := enc.Encode(m1); err != nil { log.Fatal("encode error:", err) } var m2 Message if err := dec.Decode(&m2); err != nil { log.Fatal("decode error:", err) } }四、Binaryendoding包中的binnary主要用于二进制数据序列化，但是局限性较高。注意：数据类型必须是固定大小，如果字段中有不确定大小的类型，如int，slice，string等，则会报错。字节序列方式（order）：binary.BigEndian（大端模式）：内存的低地址存放着数据高位binary.LittleEndian(小端模式)：内存的低地址存放着数据地位type Message struct { Id uint64 Size uint64 } func BinaryRW() { m1 := Message{1, 1024} buf := new(bytes.Buffer) if err := binary.Write(buf, binary.LittleEndian, m1); err != nil { log.Fatal("binary write error:", err) } var m2 Message if err := binary.Read(buf, binary.LittleEndian, &m2); err != nil { log.Fatal("binary read error:", err) } }五、ProtobufProtoBuf并不是go语言中包自带的，需要自行安装protoc编译器，以及protoc库以及生成相关的类syntax = "proto2"; package example; message Message { required uint64 id = 1; required uint64 size = 2; required string data = 3; }func ProtoEncodeDecode() { m1 := &example.Message{ Id: proto.Uint64(4), Size: proto.Uint64(1024), Data: proto.String("proto"), } buf, err := proto.Marshal(m1) if err…

Go 文件编程
type: item
date: 2020-04-07T12:14:00+00:00
description: 目录和文件管理方法由Go语言标准库的os包提供一、目录管理相关函数：func Mkdir(name string, perm FileMode) error // 创建名称为name的目录 func MkdirAll(path string, perm FileMode) error // 根据path创建多级子目录 func Remove(name string) error // 删除名称为name的目录或文件，当目录下有文件或目录时出错 func RemoveAll(path string) error // 根据path删除多级子目录，该目录下的子目录全部删除文件权限说明：采用linux系统的权限管理方式，在go语言里以0开头来8进制的数字来表示文件权限，故全权限用0777表示实例：package main import ( "fmt" "os" ) func main() { os.Mkdir("test", 0777) os.MkdirAll("test/test1/test2", 0777) err := os.Remove("test") if err != nil { fmt.Println(err) } os.RemoveAll("test") }二、文件管理相关函数：func Create(name string) (file *File, err Error) // 根据提供的文件名创建新的文件 func NewFile(fd uintptr, name string) *File //根据文件描述符和文件名创建相应的文件 func Remove(name string) Error //删除文件，和删除文件夹是同一个函数文件描述符：go程序并不是直接操作文件，而是通过操作系统提供的接口去操作文件（其它编程语言都是这样的），打开现存文件或新建文件时，操作系统内核会返回一个文件描述符（实质是一个非负整数），读写文件也需要使用文件描述符来指定待读写的文件。实例：package main import ( "fmt" "os" ) func main() { fileName := "testFile" currFile,err:=os.Create(fileName) if err!= nil { fmt.Println(err,currFile); } Close(currFile) }Go程序中有三个默认已经打开的文件：os.Stdin: 标准输入os.Stdout: 标准输出os.Stderr: 标准错误输出三、文件打开和关闭相关函数：func Open(name string) (*File, error) //Open打开一个文件用于读取 func OpenFile(name string, flag int, perm FileMode) (*File, error) //OpenFile是一个更一般性的文件打开函数 func (f *File) Close() error //Close关闭文件f，使文件不能用于读写文件打开模式：const ( O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件 O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件 O_RDWR int = syscall.O_RDWR // 读写模式打开文件 O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部 O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件 O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用，文件必须不存在 O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O O_TRUNC int = syscall.O_TRUNC // 如果可能，打开时清空文件 )实例：package main import ( "os" ) func main() { file, err := os.Open("/home/test.txt") //只读打开 if err != nil { fmt.Println("open file error: ", err) return } file.Close() //关闭文件 }目录也是可以打开和关闭的，和文件一样的四、文件操作方法详细说明os.File是一个结构体，其封装了诸多操作文件的方法： func Create(name string) (*File, error) //Create采用模式0666（任何人都可读写，不可执行）创建一个名为name的文…

Go nil
type: item
date: 2020-04-04T07:18:00+00:00
description: nil是Go中的一个使用频率很高的预声明标识符。很多种类的类型的零值都用nil表示。很多有其它语言编程经验的程序员在初学Go语言的时候常将nil看成是其它语言中的null或者NULL。这种看法只是部分上正确的，但是Go中的nil和其它语言中的null或者NULL也是有很大的区别的。一、nil是一个预声明的标识符在Go中，预声明的nil可以表示下列种类（kind）的类型的零值：指针类型（包括类型安全和非类型安全指针）映射类型切片类型函数类型通道类型接口类型二、预声明标识符nil没有默认类型Go中其它的预声明标识符都有各自的默认类型，比如预声明标识符true和false的默认类型均为内置类型bool。预声明标识符iota的默认类型为内置类型int。 但是，预声明标识符nil没有一个默认类型，尽管它有很多潜在的可能类型。事实上，预声明标识符nil是Go中唯一一个没有默认类型的类型不确定值。我们必须在代码中提供足够的信息以便让编译器能够推断出一个类型不确定的nil值的期望类型。package main func main() { // 代码中必须提供充足的信息来让编译器推断出某个nil的类型。 _ = (*struct{})(nil) _ = []int(nil) _ = map[int]bool(nil) _ = chan string(nil) _ = (func())(nil) _ = interface{}(nil) // 下面这一组和上面这一组等价。 var _ *struct{} = nil var _ []int = nil var _ map[int]bool = nil var _ chan string = nil var _ func() = nil var _ interface{} = nil // 下面这行编译不通过。 var _ = nil }三、nil值尺寸不同种类的类型的nil值的尺寸很可能不相同,一个类型的所有值的内存布局都是一样的，此类型nil值也不例外（假设此类型的零值使用nil表示）。所以同一个类型的nil值和非nil值的尺寸是一样的。但是不同类型的nil值的尺寸可能是不一样的。package main import ( "fmt" "unsafe" ) func main() { var p *struct{} = nil fmt.Println( unsafe.Sizeof( p ) ) // 8 var s []int = nil fmt.Println( unsafe.Sizeof( s ) ) // 24 var m map[int]bool = nil fmt.Println( unsafe.Sizeof( m ) ) // 8 var c chan string = nil fmt.Println( unsafe.Sizeof( c ) ) // 8 var f func() = nil fmt.Println( unsafe.Sizeof( f ) ) // 8 var i interface{} = nil fmt.Println( unsafe.Sizeof( i ) ) // 16 }上例中的输出是使用官方标准编译器编译并在64位的系统架构上运行的结果。在32位的系统架构上，这些输出值将减半。四、nil值比较两个不同类型的nil值可能不能相互比较// error: 类型不匹配 var _ = (*int)(nil) == (*bool)(nil) // error: 类型不匹配 var _ = (chan int)(nil) == (chan bool)(nil)同一个类型的两个nil值可能不能相互比较在Go中，映射类型、切片类型和函数类型是不支持比较类型。比较同一个不支持比较的类型的两个值（包括nil值）是非法的。// 下面的几个比较都编译不通过 var _ = ([]int)(nil) == ([]int)(nil) var _ = (map[string]int)(nil) == (map[string]int)(nil) var _ = (func())(nil) == (func())(nil)但是，映射类型、切片类型和函数类型的任何值都可以和类型不确定的裸nil标识符比较。// 这几行编译都没问题。 var _ = ([]int)(nil) == nil var _ = (map[string]int)(nil) == nil var _ = (func())(nil) == nil两个nil值可能并不相等如果可被比较的两个nil值中的一个的类型为接口类型，而另一个不是，则比较结果总是false。原因是，在进行此比较之前，此非接口nil值将被转换为另一个nil…