Go语言学习笔记
文档
官方文档
TopGoer
GoLang 标准库
https://studygolang.com/pkgdoc [中文]
https://pkg.go.dev/ [官方]
Go编程时光
https://golang.iswbm.com/index.html
跟煎鱼学Go
https://eddycjy.gitbook.io/golang/
C语言中文网
http://c.biancheng.net/golang/
其他:
Go 普通指针类型、unsafe.Pointer、uintptr之间的关系
https://www.cnblogs.com/-wenli/p/12682477.html
GoLang
内存逃逸
GC
Map
内置
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。
append -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close -- 主要用来关闭channel
delete -- 从map中删除key对应的value
panic -- 停止常规的goroutine (panic和recover:用来做错误处理)
recover -- 允许程序定义goroutine的panic动作
real -- 返回complex的实部 (complex、real imag:用于创建和操作复数)
imag -- 返回complex的虚部
make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy -- 用于复制和连接slice,返回复制的数目
len -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println -- 底层打印函数,在部署环境中建议使用 fmt 包
go mod
-
go mod help 查看帮助
-
go mod init初始化模块,会在项目根目录下生成 go.mod 文件。
-
go mod tidy 根据 go.mod 文件来处理依赖关系。
-
go mod vendor 将依赖包复制到项目下的 vendor 目录。建议一些使用了被墙包的话可以这么处理,方便用户快速使用命令 go build -mod=vendor 编译
-
go list -m all 显示依赖关系。go list -m -json all 显示详细依赖关系。
-
go mod download 下载依赖。参数是非必写的,path 是包的路径,version 是包的版本。
数组切片
初始化数组的初始化有多种形式,
go
[5] int {1,2,3,4,5} //长度为5的数组,其元素值依次为:1,2,3,4,5
[5] int {1,2} //长度为5的数组,其元素值依次为:1,2,0,0,0 。在初始化时没有指定初值的元素将会赋值为其元素类型int的默认值0,string的默认值是""
[...] int {1,2,3,4,5} //长度为5的数组,其长度是根据初始化时指定的元素个数决定的
[5] int { 2:1,3:2,4:3} //长度为5的数组,key:value,其元素值依次为:0,0,1,2,3。在初始化时指定了2,3,4索引中对应的值:1,2,3
[...] int {2:1,4:3} //长度为5的数组,起元素值依次为:0,0,1,0,3。由于指定了最大索引4对应的值3,根据初始化的元素个数确定其长度为5赋值与使用
数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型Slices切片(“动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片中有两个概念:一是len长度,二是cap容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得。切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
切片可以通过数组来初始化,也可以通过内置函数make()初始化 .初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容
go
s :=[] int {1,2,3 } //直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := arr[:] //初始化切片s,是数组arr的引用
s := arr[startIndex:endIndex] //将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:] //缺省endIndex时将表示一直到arr的最后一个元素
s := arr[:endIndex] //缺省startIndex时将表示从arr的第一个元素开始
s1 := s[startIndex:endIndex] //通过切片s初始化切片s1
s :=make([]int,len,cap) //通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。
注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符
切片是引用类型,在使用时需要注意其操作:
-
切片可以通过内置函数append(slice []Type,elems ...Type)追加元素,elems可以是一排type类型的数据,也可以是slice,因为追加的一个一个的元素,因此如果将一个slice追加到另一个slice中需要带上"...",这样才能表示是将slice中的元素依次追加到另一个slice中。另外在通过下标访问元素时下标不能超过len大小,如同数组的下标不能超出len范围一样。
s :=append(s,1,2,3,4) s :=append(s,s1...) -
slice的默认开始位置是0,ar[:n]等价于ar[0:n] -
slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)] -
如果从一个数组里面直接获取
slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]
slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlice和bSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。
go get 和 go get -u 的区别
如题,区别如下:
加上它可以利用网络来更新已有的代码包及其依赖包。如果已经下载过一个代码包,但是这个代码包又有更新了,那么这时候可以直接用 -u 标记来更新本地的对应的代码包。如果不加这个 -u 标记,执行 go get 一个已有的代码包,会发现命令什么都不执行。只有加了 -u 标记,命令会去执行 git pull 命令拉取最新的代码包的最新版本,下载并安装。
其他
go
func main() {
x := 0x12345678
p := unsafe.Pointer(&x) // *int -> Pointer
n := (*[4]byte)(p) // Pointer -> *[4]byte
for i := 0; i Pointer -> uintptr
p += unsafe.Offsetof(d.x) // uintptr + offset
p2 := unsafe.Pointer(p) // uintptr -> Pointer
px := (*int)(p2) // Pointer -> *int
*px = 200 // d.x = 200
fmt.Printf("%#v\n", d)
}
输出:
php
struct { s string; x int }{s:"abc", x:200}
注意:GC 把 uintptr 当成普通整数对象,它⽆法阻⽌ "关联" 对象被回收。
自定义类型
命名类型:bool、int、string
未命名类型: array、slice、map
目标类型是未命名类型会发生隐式转换否则必须显式转换,例如
go
type bigInt int64
var a bigInt = 12
var b int64 = a //报错 var b int64 = int64(a)
type FooSlice []uint
var a FooSlice = FooSlice{1, 2, 3}
var b []uint = a // 正常运行
运算符 ^取反
转换
go
func main() {
var v int64 = 12 //默认10进制
s2 := strconv.FormatInt(v, 2) //10 转2进制
fmt.Printf("%v\n", s2)
s8 := strconv.FormatInt(v, 8)
fmt.Printf("%v\n", s8)
s10 := strconv.FormatInt(v, 10)
fmt.Printf("%v\n", s10)
s16 := strconv.FormatInt(v, 16) //10 yo 16
fmt.Printf("%v\n", s16)
var sv = "11"
fmt.Println(strconv.ParseInt(sv, 16, 32)) // 16 to 10
fmt.Println(strconv.ParseInt(sv, 10, 32)) // 10 to 10
fmt.Println(strconv.ParseInt(sv, 8, 32)) // 8 to 10
fmt.Println(strconv.ParseInt(sv, 2, 32)) // 2 to 10
}
编码url
为什么需要编码和解码
1.是因为当字符串数据以url的形式传递给web服务器时,字符串中是不允许出现空格和特殊字符的;
2.因为 url 对字符有限制,比如把一个邮箱放入 url,就需要使用 urlencode 函数,因为 url 中不能包含 @ 字符;
3.url转义其实也只是为了符合url的规范而已。因为在标准的url规范中中文和很多的字符是不允许出现在url中的。
哪些字符是需要转化的呢?
1. ASCII 的控制字符
这些字符都是不可打印的,自然需要进行转化。
2. 一些非ASCII字符
这些字符自然是非法的字符范围。转化也是理所当然的了。
3. 一些保留字符
很明显最常见的就是“&”了,这个如果出现在url中了,那你认为是url中的一个字符呢,还是特殊的参数分割用的呢?
4. 就是一些不安全的字符了。
例如:空格。为了防止引起歧义,需要被转化为“+”。
明白了这些,也就知道了为什么需要转化了,而转化的规则也是很简单的。
按照每个字符对应的字符编码,不是符合我们范围的,统统的转化为%的形式也就是了。自然也是16进制的形式。
5.和字符编码无关
通过urlencode的转化规则和目的,我们也很容易的看出,urleocode是基于字符编码的。同样的一个汉字,不同的编码类型,肯定对应不同的urleocode的串。gbk编码的有gbk的encode结果。
apache等服务器,接受到字符串后,可以进行decode,但是还是无法解决编码的问题。编码问题,还是需要靠约定或者字符编码的判断解决。
因此,urleocode只是为了url中一些非ascii字符,可以正确无误的被传输,至于使用哪种编码,就不是encode所关心和解决的问题了。
编码问题,不是urlencode所要解决的。
golang之UrlEncode编码/UrlDecode解码
golang
package main
import(
"fmt"
"net/url"
)
func main() {
var urlStr string = "傻了吧:%:%@163& .html.html"
escapeUrl := url.QueryEscape(urlStr)
fmt.Println("编码:",escapeUrl)
enEscapeUrl, _ := url.QueryUnescape(escapeUrl)
fmt.Println("解码:",enEscapeUrl)
}
输出
编码: %E5%82%BB%E4%BA%86%E5%90%A7%3A%25%3A%25%40163%26+.html.html
解码: 傻了吧:%:%@163& .html.html
原文地址: golang之UrlEncode编码/UrlDecode解码
defer
Golang中的defer语句用于延迟函数的调用,每次 defer 都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。Golang 中的 defer 可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。
关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。
defer函数在return前执行
匿名返回值,返回字面量,不会被影响
func foo() int {
i := 0
defer func() {
i++
}()
return i // 0 匿名返回值
}
func gee() int {
i := 0
defer func() {
i++
}
return 0 // 0 字面量
}
具名返回值,会被影响
func bar() (result int) {
defer func() {
result++
}()
return result // 1 这里return 0或者result都会返回1
}
判断文件是否存在
go
func FileExist(path string) bool {
_, err := os.Lstat(path) // Stat
return !os.IsNotExist(err)
}
go
func FileExist(path string) bool {
err := syscall.Access(path, syscall.F_OK)
return !os.IsNotExist(err)
}
syscall.Access是针对posix平台的。不能在windows上使用
正则
-
(re) numbered capturing group
-
(?P
re) named & numbered capturing group -
(?:re) non-capturing group
-
(?flags) set flags within current group; non-capturing
-
(?flags:re) set flags during re; non-capturing
Flag syntax is xyz (set) or -xyz (clear) or xy-z (set xy, clear z). The flags are:
-
i case-insensitive (default false)
-
m multi-line mode: ^ and $ match begin/end line in addition to begin/end text (default false)
-
s let . match \
-
(default false)
-
U ungreedy: swap meaning of x* and x*?, x+ and x+?, etc (default false)
例如不区分大小写
r := regexp.MustCompile(`(?i)CaSe`)
交换顺序
a := 1
b := 2
a ^= b
b ^= a
a ^= b
a := 1
b := 2
a, b = b, a
获取正在运行的函数名
func getFuncName() string {
pc, _, _, _ := runtime.Caller(1)
return runtime.FuncForPC(pc).Name()
}
func getFuncName()string{
pc := make([]uintptr,1)
runtime.Callers(2,pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}
泛型
go >= 1.18
type Number interface {
uint | uint8 | uint16 | uint32 | uint64 # 联合类型
}
func sum[K comparable, V Number](s map[K]V) (sum uint64) {
for _, value := range s {
sum += uint64(value)
}
return
}
fmt.Println(sum[int, uint64](map[int]uint64{1: 1, 2: 3}))
为 K 类型参数指定类型约束 comparable。专门针对此类情况,comparable 在 Go 中预先声明了约束。它允许任何类型的值可以用作比较运算符 == 和的操作数 !=。Go 要求 map keys 具有可比性。所以声明 K as comparable 是必要的,这样你就可以 K 在 map 变量中用作键。它还确保调用代码对 map keys 使用允许的类型。
打印panic调用堆栈
go
defer func() {
if err := recover(); err != nil {
var buf [4096]byte
n := runtime.Stack(buf[:], true)
fmt.Println(string(buf[:n]))
}
}()
判断是否纯数字
提供了两个方法,一个逐字符判断,一个使用正则
func IsNumeric(str string) bool {
for _, v := range str {
if !unicode.IsNumber(v) { // IsNumber判断是否为一个数字字符 (类别 N)
// if !unicode.IsDigit(v) { // IsDigit 判断 r 是否为一个十进制的数字字符
return false
}
}
return true
}
# https://www.coder.work/article/193576
func IsNumericP(str string) bool {
// 使用正则判断是不是数字字符串
return regexp.MustCompile(`^[0-9]+$`).MatchString(str)
}
示例
func main() {
str := "12345"
str1 := "123rr"
// fmt.Println(IsNumeric(str), IsNumeric(str1)) // true, false
fmt.Println(IsNumericP(str), IsNumericP(str)) // true, false
}
终止协程
1.手动终止
调用 runtime.Goexit() 来手动终止协程
goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。
2.1 使用for-range退出
for-range是使用频率很高的结构,常用它来遍历数据,range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。
它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。下面这个示例程序,当in通道被关闭时,协程可自动退出。
算法
排序
冒泡
package main
import "fmt"
func BubbleSort(list []int) []int {
for i := 0; i list[i] {
list[i], list[j] = list[j], list[i]
}
}
}
return list
}
func main() {
list := []int{2, 4, 2, 6, 1, 3, 9}
fmt.Println(BubbleSort(list))
}
最大公约数 (GCD)
go
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
一个数中二进制1bit的个数
go
package main
import "fmt"
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
func main() {
//fmt.Println(PopCount(12))
fmt.Println([]byte("Hello"))
}
控制协程数量
go
package main
import (
"fmt"
"math" "runtime" "sync")
var wg = sync.WaitGroup{}
func main() {
goroutineNum := 5
taskNum := math.MaxInt32
ch := make(chan struct{})
// 先开辟好协程 等待处理数据
for i := 0; i < goroutineNum; i++ {
go func() {
for data := range ch {
fmt.Println(" i == ", data, "func == ", runtime.NumGoroutine())
wg.Done()
}
}()
}
// 分发事项
for i := 0; i < taskNum; i++ {
wg.Add(1)
ch <- struct{}{}
}
wg.Wait()
}
笔记
作用域:下面代码两个a的地址是不同的
go
a := 123
{
a := 345
}
常量:可以是编译期可以确定的数字,字符串,布尔值,len,cap, unsafe.Sizeof 等编译期可确定的值
go
const (
a, b = 1, 2
c, d
)
其中a = c = 1, b = d = 2
枚举(iota):定义常量组从0开始按行计数的自增枚举值,同一常量组中,可以提供多个iota,各自增长,如果被打断,则需要显式恢复
go
const (
// 多个iota
A, B = iota, iota << 10 // 0, 0 << 10
C, D // 1, 1 << 10
// 被打断,显式恢复
E, F = 1, 2
M, L = iota, iota << 10 // 3,包含E,F所在行
)
引用类型包括slice,map,channel,除了需要申请内存,还需要初始化相关属性
内置函数 new 计算类型⼤⼩,为其分配零值内存,返回指针。⽽ make 会被编译器翻译 成具体的创建函数,由其分配内存和初始化成员结构,返回对象⽽⾮指针
字符串是不可变值类型,内部使用指针指向UTF-8字节数组,不能使用&a[i]的形式去地址,不能修改字节数组,使用反引号定义不被转义的原始字符串
下面代码会原样输出
go
a := `Hello
\n\b\r\x00
World;
`
println(a)
连接字符串使用+,如果换行,则+应在上一行末尾,否则编译错误。单引号字符常量表⽰ Unicode Code Point,⽀持 \uFFFF、\U7FFFFFFF、\xFF 格式。 对应 rune 类型,UCS-4
go
func main() {
fmt.Printf("%T\n", 'a')
var c1, c2 rune = '\u6211', '们'
println(c1 == '我', string(c2) == "\xe4\xbb\xac")
}
要修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string。⽆论哪种转 换,都会重新分配内存,并复制字节数组,遍历字符串也有区别
go
a := "你好啊"
for i := 0; i < len(a); i++ {
fmt.Printf("%c ", a[i]) // 逐字节
}
for _, v := range a {
fmt.Printf("%c", v) // 逐字符
}
指针:指针不支持加减法运算,可以在unsafe.Pointer和任意类型指针间转换
go
a := 0x12345678
p := unsafe.Pointer(&a)
n := (*[4]byte)(p)
for i := 0; i < len(n); i++ {
fmt.Printf("%X", n[i])
}
// 78563412
函数返回局部变量指针是安全的,编译器会根据需要将其分配在GC Heap上。Pointer转为uintptr可以变相实现指针运算
go
a := struct {
Name string
X int
}{"LiBai", 1}
p := uintptr(unsafe.Pointer(&a))
p += unsafe.Offsetof(a.X)
p2 := unsafe.Pointer(p)
px := (*int)(p2)
*px = 200
fmt.Printf("%#v\n", a)
// struct { Name string; X int }{Name:"LiBai", X:200}
注意:GC 把 uintptr 当成普通整数对象,它⽆法阻⽌ "关联" 对象被回收
自定义类型
go
type (
AInt8 = uint8 // 类型别名
BInt8 uint8 // 新的类型,和uint8有相同数据存储结构,不会持有原类型的信息
)
位运算
-
0110 & 1011 = 0010 AND 都为 1。
-
0110 | 1011 = 1111 OR ⾄少⼀个为 1。
-
0110 ^ 1011 = 1101 XOR 只能⼀个为 1。
-
0110 &^ 1011 = 0100 AND NOT 清除标志位。
标志位操作
-
a := 0
-
a |= 1 << 2 // 0000100: 在 bit2 设置标志位。
-
a |= 1 << 6 // 1000100: 在 bit6 设置标志位
-
a = a &^ (1 << 6) // 0000100: 清除 bit6 标志位。
不支持运算符重载, ++/--是语句而不是表达式
初始化:初始化值以 "," 分隔。可以分多⾏,但最后⼀⾏必须以 "," 或 "}" 结尾。
go
a := []int {
1,
2,
}
控制流:代码块左⼤括号必须在条件表达式尾部
go
if a == true {
// do something
}
range:会复制对象
go
a := [3]int{0, 1, 2}
for i, v := range a {
if i == 0 {
a[1], a[2] = 999, 999
fmt.Println(a) // [0 999 999]
}
a[i] = v + 100 // 使用复制品中取出的v修改原数组 -> [100 101 102]}
fmt.Println(a)
建议改成引用类型,底层数据不会被复制
go
s := []int{1, 2, 3, 4, 5}
for i, v := range s { // 复制 struct slice { pointer, len, cap }。
if i == 0 {
s = s[:3] // 对 slice 的修改,不会影响 range。
s[2] = 100 // 对底层数据的修改。
}
println(i, v)
}
fmt.Println(s) // [1 2 100]
另外两种类型map,channel是指针包装,而不像slice是struct
分支语句switch:可使用fallthrough继续下一个分支,但是不判断条件
go
a := 1
switch a {
case 1:
fmt.Println("1")
fallthrough
case 0:
fmt.Println("0")
}
// 1 0
跳转goto:标签区分大小写,未使用的标签引发错误
defer:允许延迟调用修改返回值
go
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执⾏顺序: (z = z + 200) -> (call defer) -> (ret)
}
闭包复制原对象指针
go
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
多个defer注册,会按照FIFO次序执行,即使函数或者调用出错也会执行
go
defer func() {
panic("aaa")
}()
defer func() {
fmt.Println("bbb")
}()
fmt.Println("ccc")
// ccc
// bbb
// panic: aaa
延迟调⽤参数在注册时求值或复制,可⽤指针或闭包 "延迟" 读取
go
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引⽤
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
延迟调用中的错误可被后续延迟调用捕获,但仅最后一个错误可以被捕获
数组:值类型,赋值和传参会复制整个数组可以使用
==/!=运算
go
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组⻓度。
c := [5]int{2: 100, 4: 200} // 使⽤索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后⼀⾏的逗号。
}
值拷贝可能会造成性能问题,建议使用slice或者数组指针
slice:引用类型,自身是结构体,值拷贝传递。len,cap
go
data := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
slice := data[1:4:5]
fmt.Println(slice) // [2 3 4]
创建表达式使用的是元素的索引,个人理解: data[index:len:cap],取下标为index,长度是len的范围内的元素,例如data[1:4:5] index 1为2,len为4 取到第四个元素,即[2,3,4]
go
data := [...]int{4, 3, 5, 6, 5, 7, 3, 2, 1}
s := data[3:7]
fmt.Println(s) // [6 5 7 3]
元素读写操作的是底层数组
append 向slice追加,当append超过slice的cap时会重新分配底层数组并复制数据
go
s := make([]int, 1, 2)
fmt.Printf("%p\n", &s[0]) // 0xc00001a0a0
s = append(s, 1, 2, 3, 4, 5)
fmt.Printf("%p\n", &s[0]) // 0xc000012420
通常以 2 倍容量重新分配底层数组。在⼤批量添加数据时,建议⼀次性分配⾜够⼤的空 间,以减少内存分配和数据复制开销。或初始化⾜够⻓的 len 属性,改⽤索引号进⾏操 作。及时释放不再使⽤的 slice 对象,避免持有过期数组,造成 GC ⽆法回收。
copy 函数 copy 在两个 slice 间复制数据,复制⻓度以 len ⼩的为准。两个 slice 可指向同⼀ 底层数组,允许元素区间重叠
go
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := data[8:]
s2 := data[:5]
copy(s2, s) // dst:s2, src:s
fmt.Println(s2) // [8 9 2 3 4]
fmt.Println(data) // [8 9 2 3 4 5 6 7 8 9]