在Go语言中,数组是一种固定长度的数据结构,一旦创建,长度就不能改变。这在某些情况下可能会限制它的使用。为了提供更灵活的数据结构,Go引入了切片(slice),它基于数组实现,但具有动态调整大小的能力。
切片的内部结构在src/runtime/slice.go
中定义,如下图,它包含三个主要部分:
切片在 Go 语言中有多种初始化方式,每种方式都有其特点和适用场景。下面简要介绍一下这些初始化方式:
govar s []int
gos := []int{1, 2, 3, 4, 5}
gos := make([]int, 5) // 创建长度为 5 的切片
goarr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // 截取索引为 1~2 的部分作为新切片
在底层,这些初始化方式会调用相应的函数来完成内存的分配和初始化工作。例如,make 函数实际上调用了 runtime.makeslice 函数来计算所需内存大小,并分配对应的内存空间。这些底层函数负责处理内存管理,确保切片的正确分配和初始化,使得开发者能够更方便地使用切片来操作数据。
切片的内存管理是其高效性的关键之一。切片是基于底层数组的动态大小视图,它通过指针、长度和容量来描述底层数组的一部分。
当切片的长度 len 小于容量 cap 时,我们可以通过追加元素来扩展切片,而不需要重新分配整个底层数组。当我们追加一个元素到切片时,如果新的长度仍然小于容量,则内部的指针和长度会被更新,但是底层数组不会改变。这样,我们可以保持对同一个底层数组的引用,避免了内存的重新分配和复制,提高了效率。
当切片的长度超过容量时,追加元素将导致底层数组的重新分配和复制。这个时候,Go会分配一个新的更大的底层数组,并将原数组中的元素复制到新数组中。这个过程在runtime.growslice函数中实现。新的底层数组通常会比原来的大一些,以容纳更多的元素。这时,切片的指针和长度会指向新的底层数组,旧的底层数组会被垃圾回收。
我们先通过一个简单的例子来观察切片的动态特性。在这个例子中,我们可以看到切片在追加元素时如何动态调整其大小。
goimport "fmt"
func main() {
slice := make([]int, 0, 4) // 初始化一个长度为0,容量为3的切片
slice = append(slice, 1, 2) // 追加元素
newSlice := append(slice, 3) // First
fmt.Printf("First, slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, slice, len(slice), cap(slice))
fmt.Printf("First, newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, newSlice, len(newSlice), cap(newSlice))
slice = append(slice, 4) // Second
fmt.Printf("Second, slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, slice, len(slice), cap(slice))
fmt.Printf("Second, newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, newSlice, len(newSlice), cap(newSlice))
slice = append(slice, 5, 6) // Third
fmt.Printf("Third, slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, slice, len(slice), cap(slice))
fmt.Printf("Third, newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, newSlice, len(newSlice), cap(newSlice))
newSlice[1] = 10 // Fourth
fmt.Printf("Fourth, slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, slice, len(slice), cap(slice))
fmt.Printf("Fourth, newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, newSlice, len(newSlice), cap(newSlice))
}
// output
// First, slice = [1 2], Pointer = 0xc000132000, len = 2, cap = 4
// First, newSlice = [1 2 3], Pointer = 0xc000132000, len = 3, cap = 4
// Second, slice = [1 2 4], Pointer = 0xc000132000, len = 3, cap = 4
// Second, newSlice = [1 2 4], Pointer = 0xc000132000, len = 3, cap = 4
// Third, slice = [1 2 4 5 6], Pointer = 0xc0001240c0, len = 5, cap = 8
// Third, newSlice = [1 2 4], Pointer = 0xc000132000, len = 3, cap = 4
// Fourth, slice = [1 2 4 5 6], Pointer = 0xc0001240c0, len = 5, cap = 8
// Fourth, newSlice = [1 10 4], Pointer = 0xc000132000, len = 3, cap = 4
这里也简单介绍下Go的切片扩容策略
当一个切片不再被任何变量引用时,垃圾回收器会自动回收其占用的内存,包括其底层数组的内存(前提是没有其他切片或数据结构引用该底层数组)。切片的生命周期由垃圾回收器管理,当切片变量超出作用域或被显式设置为 nil 时,垃圾回收器将其标记为可回收对象。在下一次垃圾回收周期中,如果没有其他引用存在,相关内存会被释放。
由于slice的底层是数组,很可能数组很大,但slice所取的元素数量却很小,这就导致数组占用的绝大多数空间是被浪费的。
比如下面的代码,如果传入的slice b是很大的,然后引用很小部分给全局量a,那么b未被引用的部分就不会被释放,造成了所谓的内存泄漏。只要全局量a在,b就不会被回收。
govar a []int
func makSlice(b []int) {
a = b[:1] // 和b共用一个底层数组
return
}
解决方式:如果我们只用到一个slice的一小部分,那么底层的整个数组也将继续保存在内存中。当这个底层数组很大,或者类似这样的场景很多时,可能会造成内存增加,造成崩溃。在这样的情况下,我们可以将需要的分片复制到一个新的slice中去,减少内存的占用。
append 是 Go 语言中的一个内置函数,用户可以直接调用它来操作切片,而不需要引入额外的包。append 函数的定义位于 Go 语言的源码包 builtin 中的 builtin.go 文件中。虽然我们不能直接修改内置函数,但理解其工作原理可以帮助我们更好地使用它。
go// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
该文件中仅仅定义了函数签名,并没有包含函数实现的任何代码。这里我想要深究一下,append究竟是如何实现的呢?
go// append函数的简化实现(伪代码)
func append(slice []T, elems ...T) []T {
var newSlice []T
newLen := len(slice) + len(elems)
if newLen <= cap(slice) {
// 容量足够,直接追加
newSlice = slice[:newLen]
} else {
// 容量不足,分配新的底层数组
newCap := newLen
if newCap < 2*len(slice) {
newCap = 2 * len(slice)
}
newSlice = make([]T, newLen, newCap)
copy(newSlice, slice)
}
copy(newSlice[len(slice):], elems)
return newSlice
}
Go 编译过程大致可以分为以下四个阶段:词法与语法分析、类型检查与抽象语法树(AST)转换、中间代码生成和生成最终的机器码。
编译的第二和第三阶段的代码,分别是位于 cmd/compile/internal/typecheck/typecheck.go
下的类型检查逻辑
gofunc typecheck1(n *Node, top int) (res *Node) {
...
switch n.Op {
case ir.OAPPEND:
n := n.(*ir.CallExpr)
return tcAppend(n)
}
位于cmd/compile/internal/walk/assign.go
下的抽象语法树转换逻辑
gofunc walkAssign(init *ir.Nodes, n ir.Node) ir.Node {
case ir.OAPPEND:
// x = append(...)
call := as.Y.(*ir.CallExpr)
if call.Type().Elem().NotInHeap() {
base.Errorf("%v can't be allocated in Go; it is incomplete (or unallocatable)", call.Type().Elem())
}
var r ir.Node
switch {
case isAppendOfMake(call):
// x = append(y, make([]T, y)...)
r = extendSlice(call, init)
case call.IsDDD:
r = appendSlice(call, init) // also works for append(slice, string).
default:
r = walkAppend(call, init, as)
}
as.Y = r
if r.Op() == ir.OAPPEND {
r := r.(*ir.CallExpr)
// Left in place for back end.
// Do not add a new write barrier.
// Set up address of type for back end.
r.X = reflectdata.AppendElemRType(base.Pos, r)
return as
}
// Otherwise, lowered for race detector.
// Treat as ordinary assignment.
}
}
位于 cmd/compile/internal/ssagen/ssa.go
的中间代码生成逻辑
go// append converts an OAPPEND node to SSA.
// If inplace is false, it converts the OAPPEND expression n to an ssa.Value,
// adds it to s, and returns the Value.
// If inplace is true, it writes the result of the OAPPEND expression n
// back to the slice being appended to, and returns nil.
// inplace MUST be set to false if the slice can be SSA'd.
// Note: this code only handles fixed-count appends. Dotdotdot appends
// have already been rewritten at this point (by walk).
func (s *state) append(n *ir.CallExpr, inplace bool) *ssa.Value {
// If inplace is false, process as expression "append(s, e1, e2, e3)":
//
// ptr, len, cap := s
// len += 3
// if uint(len) > uint(cap) {
// ptr, len, cap = growslice(ptr, len, cap, 3, typ)
// Note that len is unmodified by growslice.
// }
// // with write barriers, if needed:
// *(ptr+(len-3)) = e1
// *(ptr+(len-2)) = e2
// *(ptr+(len-1)) = e3
// return makeslice(ptr, len, cap)
//
//
// If inplace is true, process as statement "s = append(s, e1, e2, e3)":
//
// a := &s
// ptr, len, cap := s
// len += 3
// if uint(len) > uint(cap) {
// ptr, len, cap = growslice(ptr, len, cap, 3, typ)
// vardef(a) // if necessary, advise liveness we are writing a new a
// *a.cap = cap // write before ptr to avoid a spill
// *a.ptr = ptr // with write barrier
// }
// *a.len = len
// // with write barriers, if needed:
// *(ptr+(len-3)) = e1
// *(ptr+(len-2)) = e2
// *(ptr+(len-1)) = e3
}
上面这个函数的入参 inplace 代表返回值是否覆盖原变量,如果是true,例如 slice = append(slice, 1, 2, 3)
语句,那么返回值会覆盖原变量。
不管 inpalce 是否为true,我们均会获取切片的数组指针、大小和容量,如果在追加元素后,切片新的大小大于原始容量,就会调用 runtime.growslice
对切片进行扩容,并将新的元素依次加入切片。
因此,通过 append向 元素类型为 int 的切片追加元素 1 可分为两种情况。
前面提到追加操作时,当切片底层数组的剩余空间不足以容纳追加的元素,就会调用 growslice(实现在 runtime 包中)。growslice 函数的主要任务是分配一个新的底层数组,并将旧数组的数据复制到新数组中。其调用的入参 cap 为追加元素后切片的总长度。
go// growslice函数的简化实现(伪代码)
func growslice(slice []T, newLen int) []T {
oldCap := cap(slice)
newCap := oldCap
// 扩展容量,通常是原来容量的两倍
if newLen > oldCap {
newCap = oldCap * 2
if newCap < newLen {
newCap = newLen
}
}
// 分配新的底层数组
newSlice := make([]T, newLen, newCap)
// 将旧数组的数据复制到新数组中
copy(newSlice, slice)
return newSlice
}
具体说来,我们可以根据逻辑分为三个部分。
gofunc growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
}
如果需要的容量 cap 超过原切片容量的两倍 doublecap,会直接使用需要的容量作为新容量newcap。否则,当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
通过判断切片元素的字节大小是否为1,系统指针大小(32位为4,64位为8)或2的倍数,进入相应所需内存大小的计算逻辑。
govar overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
lenmem = uintptr(oldLen)
newlenmem = uintptr(newLen)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == goarch.PtrSize:
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if goarch.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31
}
lenmem = uintptr(oldLen) << shift
newlenmem = uintptr(newLen) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
capmem = uintptr(newcap) << shift
default:
lenmem = uintptr(oldLen) * et.size
newlenmem = uintptr(newLen) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
capmem = uintptr(newcap) * et.size
}
这里需要注意的是 roundupsize 函数,它根据输入期望大小 size ,返回 mallocgc 实际将分配的内存块的大小。
Go 语言的内存分配器在处理不同大小的内存分配请求时,使用了不同的策略来提高分配效率并减少内存碎片。roundupsize 函数是内存分配器中的一个关键函数,用于根据输入的期望大小(size)返回实际将分配的内存块大小。
具体来说,roundupsize 函数根据内存分配请求的大小(size),决定分配的内存块大小。如果请求的内存大小是小对象(小于 32KB),则将其向上取整到内存分配器的一个类(class)大小。如果是大对象,则将其向上取整到虚拟内存页大小(通常是 4KB)的倍数。
小对象内存分配 对于小对象(小于 32KB),内存分配器使用 class_to_size、size_to_class8 和 size_to_class128 数组来进行内存大小的向上取整。这些数组定义了不同大小类(class)的内存块大小,以便快速分配和减少内存碎片。
通过这些数组,内存分配器可以快速确定要分配的内存块大小,减少内存碎片。
大对象内存分配 对于大对象(大于等于 32KB),内存分配器使用 alignUp 函数将请求的内存大小向上取整到虚拟内存页大小(通常是 4KB)的倍数。
总结 通过 roundupsize 函数,Go 语言的内存分配器能够根据请求的内存大小返回实际将分配的内存块大小。对于小对象,使用 class_to_size、size_to_class8 和 size_to_class128 数组来快速确定内存块大小;对于大对象,使用 alignUp 函数将请求的内存大小向上取整到虚拟内存页大小的倍数。这些策略有助于提高内存分配效率并减少内存碎片。
go if overflow || capmem > maxAlloc {
panic(errorString("growslice: len out of range"))
}
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from oldLen to newLen.
// Only clear the part that will not be overwritten.
// The reflect_growslice() that calls growslice will manually clear
// the region not cleared here.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in oldPtr since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata)
}
}
//if oldPtr != nil {
// println(100)
//} else {
// println(10000)
//}
memmove(p, oldPtr, lenmem)
return slice{p, newLen, newcap}
如果上个环节中,造成了溢出或者期望分配的内存超过最大分配限制,会引起 panic。
mallocgc 函数分配一个大小为之前计算得到的 capmem 对象。如果是小对象,则直接从当前G所在P的缓存空闲列表中分配;如果是大对象,则从堆上进行分配。同时,如果切片中的元素不是指针类型,那么会调用 memclrNoHeapPointers 将超出切片当前长度的位置清空;如果元素是指针类型,且原有切片元素个数不为 0 并可以打开写屏障时,需要调用 bulkBarrierPreWriteSrcOnly 将旧切片指针标记隐藏,在新切片中保存为nil指针。最后使用memmove将原数组内存中的内容拷贝到新申请的内存中,并将新的内存指向指针p 和旧的长度值,新的容量值赋值给新的 slice 并返回。
注意,在 growslice 完成后,只是把旧有数据拷贝到了新的内存中去,计算得到新的 slice 容量大小,此时新的slice已经拷贝了旧的slice数据,并且其底层数组有充足的剩余空间追加数据。后续只需拷贝追加数据至剩余空间,并修改 len 值即可。
由于切片在内存管理上的特殊性,在某些情况下(频繁读写切片)可能对性能影响很大。我们通常会采取下面这些的优化措施使程序运行得更加高效。
在切片容量不足时,append 操作会导致底层数组的重新分配和复制,这会带来显著的性能开销。所以在创建切片时,预分配足够的容量可以避免多次扩容操作。
slice := make([]int, 0, 100)
在处理切片时,不必要的切片操作会增加额外的开销。例如,频繁地创建切片的子集,或者在循环中不断地追加元素,都可能导致性能下降。
每次创建切片的子集都会生成一个新的切片头部(包含指向底层数组的指针、长度和容量),但底层数组不会被复制。如果子切片的生命周期超过原始切片,可能会导致底层数组无法被垃圾回收,从而增加内存使用。
切片不支持并发读写,所以并不是线程安全的,使用多个 goroutine 对类型为 slice 的变量进行操作,每次输出的值大概率都不会一样,与预期结果也不一致。
因为当多个 goroutine 并发地对同一个切片执行 append 操作时,如果底层数组的容量不足而导致切片扩容,就会出现数据竞争的问题。这会导致多个 goroutine 的 append 结果被覆盖,从而导致最终切片的长度小于预期的 n。
数据竞争问题
- 多个 goroutine 同时检查切片的容量,发现容量不足。
- 多个 goroutine 同时分配新的底层数组,并将旧数组的数据复制到新数组中。
- 最终只有一个 goroutine 的 append 结果被保留,其他 goroutine 的结果被覆盖。
常见的解决方案包括使用互斥锁(sync.Mutex)或者通道(chan)来同步对切片的访问。
在多线程环境中,为了避免数据竞争问题,可以使用互斥锁(mutex)来保护对切片的访问。
goimport (
"fmt"
"sync"
)
func main() {
a := make([]int, 0)
var lock sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
lock.Lock()
defer lock.Unlock()
a = append(a, i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println((len(a))) // 1000。如果不加锁,大概率是小于1000的9xx
}
通过通道来同步对切片的访问,将所有的 append 操作都交给一个专门的 goroutine 来处理。通过通道实现切片线程安全,适合对性能要求高的场景。
goimport (
"fmt"
)
func main() {
buffer := make(chan int)
a := make([]int, 0)
// 消费者
go func() {
for v := range buffer {
a = append(a, v)
}
}()
// 生产者
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
buffer <- i
}(i)
}
wg.Wait()
fmt.Println(len(a)) // 10000
}
切片还可以用于实现更复杂的数据结构和算法,以下是一些切片的高级应用示例。
切片可以很容易地实现队列(FIFO)的功能。通过在切片的末尾追加元素,并从对首移除元素,我们可以创建一个队列。
goimport "fmt"
type Queue struct {
slice []int
}
func (q *Queue) Enqueue(value int) {
q.slice = append(q.slice, value)
}
func (q *Queue) Dequeue() (int, bool) {
if len(q.slice) == 0 {
return 0, false
}
value := q.slice[0]
q.slice = q.slice[1:]
return value, true
}
func main() {
q := Queue{}
q.Enqueue(1)
q.Enqueue(2)
for {
value, ok := q.Dequeue()
if !ok {
break
}
fmt.Println(value)
}
}
在下面这个例子中,我们定义了一个Stack结构体,它包含一个切片items,用于存储栈中的元素。我们实现了 Pus h和 Pop 方法来操作栈。
goimport "fmt"
type Stack struct {
items []interface{}
}
func (s *Stack) Push(item interface{}) {
s.items = append(s.items, item)
}
func (s *Stack) Pop() interface{} {
if len(s.items) == 0 {
return nil
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
func main() {
stack := Stack{}
stack.Push(1)
stack.Push("hello")
stack.Push(true)
for {
item := stack.Pop()
if item == nil {
break
}
fmt.Println(item)
}
}
在下面这个例子中,我们定义了一个BSTNode结构体来表示二叉搜索树的节点,并实现了插入和搜索功能。
gotype BSTNode struct {
Value int
Left *BSTNode
Right *BSTNode
}
// BSTInsert 用于向BST中插入新值
func BSTInsert(root *BSTNode, value int) {
if root == nil {
return &BSTNode{Value: value}
}
if value < root.Value {
root.Left = BSTInsert(root.Left, value)
} else if value > root.Value {
root.Right = BSTInsert(root.Right, value)
}
return root
}
// BSTSearch 用于在BST中搜索特定值
func BSTSearch(root *BSTNode, value int) bool {
if root == nil {
return false
}
if root.Value == value {
return true
}
if value < root.Value {
return BSTSearch(root.Left, value)
}
return BSTSearch(root.Right, value)
}
func main() {
root := &BSTNode{Value: 2}
BSTInsert(root, 1)
BSTInsert(root, 3)
fmt.Println(BSTSearch(root, 1)) // 输出:true
fmt.Println(BSTSearch(root, 3)) // 输出:false
}
在处理大型数据集时,使用迭代器可以提高代码的可读性和效率。Go语言的切片没有内置的迭代器,但我们可以通过编写自定义函数来模拟迭代器的行为。以下是一个简单的切片迭代器的示例,它允许我们遍历切片中的每个元素,而不需要直接操作索引。
goimport "fmt"
// SliceIterator 是一个自定义的切片迭代器
type SliceIterator struct {
slice []int
index int
}
// NewSliceIterator 创建一个新的切片迭代器
func NewSliceIterator(slice []int) *SliceIterator {
return &SliceIterator{slice: slice, index: 0}
}
// HasNext 检查迭代器是否还有更多的元素
func (i *SliceIterator) HasNext() bool {
return i.index < len(i.slice)
}
// Next 返回下一个元素,并更新迭代器的索引
func (i *SliceIterator) Next() int {
if i.HasNext() {
value := i.slice[i.index]
i.index++
return value
}
panic("没有更多元素")
}
func main() {
slice := []int{1, 2, 3, 4, 5}
iterator := NewSliceIterator(slice)
for iterator.HasNext() {
fmt.Println(iterator.Next())
}
}
在这个例子中,我们创建了一个SliceIterator结构体,它包含了切片和当前索引。通过HasNext和Next方法,我们可以遍历切片中的所有元素。
在处理数据流时,切片可以作为一种缓冲机制,帮助我们管理数据的读取和写入。这在文件操作、网络通信等场景中尤为常见。以下是一个使用切片读取文件内容的示例,在读取或写入文件时,我们使用切片来临时存储数据块。
goimport (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer file.Close()
reader := bufio.NewReader(file)
// 使用切片作为缓冲区读取文件
buffer := make([]byte, 1024)
for {
n, err := reader.Read(buffer)
if err != nil {
if err != nil {
panic(err)
}
break
}
// 处理读取的数据
fmt.Print(string(buffer[:n]))
}
}
在这个例子中,我们使用bufio.Reader来逐块读取文件,每次读取1024字节到切片buffer中,并处理这些数据。
本文作者:sora
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!