LOADING

正在加载

Go基础学习(叁)——复杂类型

壹 介绍

在Go语言中,还存在着一些复杂的类型(引用类型):数组、切片和集合。

贰 数组array

2.1 简介

Go 语言提供了数组类型的数据结构。数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。相对于去声明 number0, number1, ..., number99 的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99] 更加方便且易于扩展。数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的底层就是就是指针。
a01f5fce809acf7697529217ac55cb23.png

注意:需要区分数组与切片,数组在方括号里面是有值的,切片是没有值的,切片后面会讲。

2.2 声明数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:

// 一维数组
var 数组变量名 [元素数量] 数组类型
// 多维数组
var 数组变量名 [元素数量1][元素数量2]...[元素数量N] 数组类型

例子:

// 定义了一维数组 balance 长度为 10 类型为 float32
var balance [10]float32
// 声明了二维的整型数组
var double [5][10]int
// 声明了三维的整型数组
var threedim [5][10][4]int

二维数组是最简单的多维数组,二维数组本质上是由一维数组组成的,三维数组也是如此。
二维数组可认为是一个表格,x 为行,y 为列,下图演示了一个二维数组 a 为三行四列:
1615f947adece701d6aed9da4e6ad602.png
以此类推三维数组就有点像一个立方体。

2.3 初始化数组

为什么要初始化?举个例子,我们脑海里想了一个玩具,这个玩具,现在是幻想出来,也就是编程里面的定义,那要想完这个玩具,那我们就要买这个玩具或者制造这个玩具,这个叫初始化或者叫实例化。所以任何的类型变量都需要定义,然后初始化,才可以使用。

  • 一维数组初始化:
    ```go
    // 数组初始化
    var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
    // 如果数组初始化不赋值那么会自动赋为0|False|空字符串
    var testArray [3]int
    // 也可以通过字面量在声明数组的同时快速初始化数组
    balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
    // 如果数组长度不确定,可以使用…代替数组的长度,编译器会根据元素个数自行推断数组的长度
    var balance = […]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
    balance := […]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
    // 如果设置了数组的长度,我们还可以通过指定下标来初始化元素,将索引为 1 和 3 的元素初始化
    balance := [5]float32{1:2.0,3:7.0}
    // 初始化数组中{}中的元素个数不能大于[]中的数字。如果忽略[]中的数字不设置数组大小,Go语言会根据元素的个数来设置数组的大小
    balance[4] = 50.0

// 还可以利用循环初始化
var strarr [3]int
for i := 0; i < 3; i++ {
strarr[i] = i
fmt.Printf(“数组第%d个元素是%d\n”, i, strarr[i])
}

// 如果是以下定义,那么是定义切片,后面会讲
var str := []int{}

//可以看到格式类似
var 数组变量名 = [数组长度]数组类型{值}

上面的例子结果如下:
![6b4ae9fb55a726fcc39c40dea64b1341.png](/images/Go基础编程/6b4ae9fb55a726fcc39c40dea64b1341.png)

- 二维数组初始化:
多维数组可通过大括号来初始值。以下实例为一个 3 行 4 列的二维数组:
```go
//可以看出二维数组由两个大括号组成,最里面的括号相对于外面的括号是一个值,以此类推,多级数组
a := [3][4]int{
        {0, 1, 2, 3}, // 第一行索引为0
        {4, 5, 6, 7}, // 第二行索引为1
        {8, 9, 10, 11}, // 第三行索引为2
    }
//我们还可以不计算元素个数初始化
a := [...][4]int{
        {0, 1, 2, 3}, // 第一行索引为0
        {4, 5, 6, 7}, // 第二行索引为1
        {8, 9, 10, 11}, // 第三行索引为2
    }
//注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。以下不准确:
// a := [3][...]int{
// 		{0, 1, 2, 3},
// 		{4, 5, 6, 7},
// 		{8, 9, 10, 11},
// 	}

以此类推,三维数组则是三个大括号叠加。

2.4 访问数组元素

数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。

  • 访问一维数组:

例如:

数组名[索引值]

例子:

package main

import "fmt"

func main() {
    array := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
    for i := 0; i < 10; i++ {
        fmt.Printf("%d值为:%v\n", i, array[i])
    }
}

22e3938cb4ed045f9f64c3b3e252abb0.png

  • 访问二维数组:

二维数组通过指定坐标来访问。如数组中的行索引与列索引,例如:

数组名[索引值1][索引值2]

例子:

package main

import "fmt"

func main() {
    // 创建空的二维数组
    animals := [][]string{}
    // 创建三个一维数组,各数组长度不同
    row1 := []string{"fish", "shark", "eel"}
    row2 := []string{"bird"}
    row3 := []string{"lizard", "salamander"}
    // append()函数主要用于给某个切片追加元素
    // 使用 append() 函数将一维数组添加到二维数组中
    animals = append(animals, row1)
    animals = append(animals, row2)
    animals = append(animals, row3)
    // 循环输出
    for i := range animals {
        fmt.Printf("Row: %v\n", i)
        fmt.Println(animals[i])
    }
}

6c6c78b415a5a185004b38993309600e.png

注意:

  • a.数组是值类型,所以在执行赋值和传参操作时是将复制整个数组到另一个变量中。因此改变复制后数组的值,不会改变原先数组的值。
  • b.数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
  • c.[n]*T表示指针数组,*[n]T表示数组指针 。

叁 切片slice

3.1 简介

因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,Go 中提供了一种灵活,功能强悍的内置类型切片(”动态数组”)——Go 语言切片,Go 语言切片是对数组的抽象,是一个拥有相同类型元素的可变长度的序列,切片的底层就是一个数组。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容,是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

切片的本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。
0b67a5feb4d683fbeaf4940e3c87ad8b.png
切片s2 := a[3:6],相应示意图如下:
60174a69142a1db34e3725bae1840a69.png

3.2 定义切片

你可以声明一个未指定大小的数组来定义切片:

var 切片名 []切片类型
//name:表示变量名
//type:表示切片中的元素类型
//切片不需要说明长度
//可以使用make()函数来创建切片:
var name []type = make([]type, len)
//也可以简写为:
name := make([]type, len)

切片拥有自己的长度和容量,可以指定容量, length是当前容量,capacity是最大容量,其中capacity为可选参数,默认等于length。我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

make([]T, length, capacity)

为什么切片需要容量?

  • a.可以隐藏数组中暂时不使用的空间
  • b.当使用数组达到闸值时,重新分配空间,实现动态数组效果

3.3 切片初始化

切片的初始化比较灵活:

//直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3,一般使用这种方式定义
s :=[]int{1,2,3} 
//初始化切片 s,是数组 arr 的引用
s := arr[:]
//将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex] 
//默认 endIndex 时将表示一直到arr的最后一个元素,默认为切片操作数的长度
s := arr[startIndex:] 
//默认 startIndex 时将表示从 arr 的第一个元素开始,默认为0
s := arr[:endIndex] 
//通过切片 s 初始化切片 s1
s1 := s[startIndex:endIndex]
//通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片
s :=make([]int,len,cap)
// 利用循环初始化
// 需要注意这个初始化的时候是在make的容量内,如果超过容量范围需要进行增加操作,后面会讲
strarr := make([]int, 3)
for i := 0; i < 3; i++ {
    strarr[i] = i
    fmt.Printf("数组第%d个元素是%d\n", i, strarr[i])
}

make函数使用:链接

3.4 空(nil)切片

一个切片在未初始化之前默认为 nil,长度为 0,实例如下:

package main
import "fmt"
func main() {
    var numbers []int
    fmt.Printf("len=%d cap=%d slice=%v\n", len(numbers), cap(numbers), numbers)
    if numbers == nil {
        fmt.Printf("切片是空的")
    }
}

98a8331125ff8b67eeab6099ac3982d3.png

len函数使用:链接
cap函数使用:链接

3.5 切片截取与遍历

可以通过设置下限、上限和容量(cap表示实际的容量,可以忽略)来设置截取切片 [lower-bound:upper-bound:[cap]],实例如下:

package main
import "fmt"
func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
func main() {
    // 创建切片
    numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
    printSlice(numbers)
    // 打印原始切片
    fmt.Println("numbers ==", numbers)
    // 打印子切片从索引1(包含) 到索引4(不包含)
    fmt.Println("numbers[1:4] ==", numbers[1:4])
    // 默认下限为0
    fmt.Println("numbers[:3] ==", numbers[:3])
    // 默认上限为len(s),而不是cap(s)
    fmt.Println("numbers[4:] ==", numbers[4:])
    numbers1 := make([]int, 0, 5)
    printSlice(numbers1)
    // 打印子切片从索引0(包含)到索引2(不包含)
    number2 := numbers[:2]
    printSlice(number2)
    // 打印子切片从索引 2(包含) 到索引 5(不包含)
    number3 := numbers[2:5]
    printSlice(number3)
    // 对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式
    // numbers[low : high : max]
}

66d9f9eed63f41f399ccda99fed9ca8d.png

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历,这里就不进行赘述。

3.6 切片的其他操作

  • a.增加
    关于切片增加操作是使用函数append链接
  • b.复制
    关于切片复制操作的说明:链接
  • c.删除
    Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...), 代码如下:
    package main
    import "fmt"
    func main() {
      // 从切片中删除元素
      a := []int{30, 31, 32, 33, 34, 35, 36, 37}
      // 要删除索引为2的元素
      a = append(a[:2], a[3:]...)
      fmt.Println(a) //[30 31 33 34 35 36 37]
    }
    
  • d.切片不能直接比较
    切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,因为切片可以包含0,所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断

肆 集合map

Go语言中提供的映射关系容器为map,Map 是一种无序的基于key-value的数据结构,其内部使用散列表(hash)实现,我们无法决定它的返回顺序。Map是引用类型,必须初始化才能使用。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它,所以map底层就是指针

4.1 定义 map

首先我们来定义map,使用 map 关键字来定义 map:

map[KeyType]ValueType
//KeyType:表示键的类型
//ValueType:表示键对应的值的类型
//map类型的变量默认初始值为nil

需要使用make()函数来分配内存,map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

map_name := make(map[key_type]value_type, [cap])
//其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

如果不初始化 map,那么就会创建一个 nil mapnil map 不能用来存放键值对。

4.2 map初始化

Map的初始化比较灵活,直接使用map插入key - value对:

package main

import "fmt"

func main() {
    var countryCapitalMap map[string]string /*创建集合 */
    countryCapitalMap = make(map[string]string)
    /* map插入key - value对,各个国家对应的首都 */
    countryCapitalMap["France"] = "巴黎"
    countryCapitalMap["Italy"] = "罗马"
    countryCapitalMap["Japan"] = "东京"
    countryCapitalMap["India "] = "新德里"
    /* map也支持在声明的时候填充元素 */
    userInfo := map[string]string{
        "username": "沙河小王子",
        "password": "123456",
    }
    fmt.Println(userInfo)
    fmt.Println(countryCapitalMap)
}

f5d445a9bfb48854059aca2fbd66b1ff.png

4.3 map的遍历

package main

import "fmt"

func main() {
    // 定义一个map
    userInfo := map[string]string{
        "username": "A7cc",
        "password": "123456",
    }
    // 使用for range遍历map
    for k, v := range userInfo {
        fmt.Println(k, "值是:", v)
    }
    // 只遍历key
    for k := range userInfo {
        fmt.Println(k, "值是:", userInfo[k])
    }
}

02b5660f0f6e464517d2c2500fbf03e5.png
可以看到直接遍历时,map是按照首字符的ASCII码输出的。

4.4 判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

value, ok := map[key]

举个例子:

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    // 如果key存在,ok为true,v为对应的值;不存在,ok为false,v为0
    if v, ok := scoreMap["张三"];ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
    // 可以看到上面的v, ok := scoreMap["张三"]在go语言中是可以放到if的判断语句中,个人感觉if判断语句前面有表达式就是为这种情况而生的
}

4.5 元素为map类型的切片

下面的代码演示了切片中的元素为map类型时的操作:

func main() {
    var mapSlice = make([]map[string]string, 3)
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
    fmt.Println("after init")
    // 对切片中的map元素进行初始化
    mapSlice[0] = make(map[string]string, 10)
    mapSlice[0]["name"] = "小王子"
    mapSlice[0]["password"] = "123456"
    mapSlice[0]["address"] = "沙河"
    for index, value := range mapSlice {
        fmt.Printf("index:%d value:%v\n", index, value)
    }
}

4.6 值为切片类型的map

下面的代码演示了map中值为切片类型的操作:

func main() {
    var sliceMap = make(map[string][]string, 3)
    fmt.Println(sliceMap)
    fmt.Println("after init")
    key := "中国"
    value, ok := sliceMap[key]
    if !ok {
        value = make([]string, 0, 2)
    }
    value = append(value, "北京", "上海")
    sliceMap[key] = value
    fmt.Println(sliceMap)
}

4.7 map的其他操作

  • a.删除
    关于map删除操作的说明:链接

伍 通用函数

5.1 make函数

内建函数 make 用来为 slicemapchan 类型分配一块内存空间和初始化(注意:只能用在这三种类型上)。

格式:

valname := make(Type, len)
  • Type:就是定义的类型,即slice、map、chan其中一个
  • len:就是分配该类型的长度,但在slice中可以在后面加个cap代表容量
  • 返回值为创建的类型,个人感觉就是这块地址的指针,这个概念后面会讲,这里只是用做理解

实例:

package main

import "fmt"

func main() {
    // 申请一个长度为5,容量为10的切片
    mySlice := make([]int, 5, 10)
    // 申请一个长度为3的map
    mymap := make(map[string]string, 3)
    fmt.Println(mySlice)	//[0 0 0 0 0]
    fmt.Println(mymap)	//map[],因为map没有初始化,所以没有值
}

5.1 len函数

切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。

格式:

vallen := len(v)
  • v:就是要返回长度的对象
  • 返回值为该对象长度

实例:

package main
import (
    "fmt"
)
func main() {
    s1 := [...]int{1, 2, 3}
    s2 := []int{1, 2, 3}
    s3 := make(map[string]string)
    s3["France"] = "巴黎"
    s3["Italy"] = "罗马"
    s3["Japan"] = "东京"
    s3["India"] = "新德里"
    fmt.Printf("%T,%T,%T\n", s1, s2, s3)
    fmt.Printf("%d,%d", cap(s1), cap(s2))
}

381340c9800a6eaba6700856a3eadc17.png

5.2 cap函数

cap()函数返回的是数组切片分配的空间大小。

格式:

valcao := cap(v)
  • v:就是要返回长度的对象,一般是切片
  • 返回值为该对象容量

实例:

package main

import "fmt"

func main() {
    // 申请一个长度为5,容量为10的切片
    mySlice := make([]int, 5, 10)
    fmt.Println("len(mySlice):", len(mySlice))
    fmt.Println("cap(mySlice):", cap(mySlice))
}

5.3 append函数

如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。

package main
import "fmt"
func main() {
    var numbers []int
    printSlice(numbers)
    //len=0 cap=0 slice=[]

    // 通过var声明的零值切片可以在append()函数直接使用,无需初始化。
    // 允许追加空切片
    numbers = append(numbers, 0)
    printSlice(numbers)
    //len=1 cap=1 slice=[0

    // 向切片添加一个元素
    numbers = append(numbers, 1)
    printSlice(numbers)
    //len=2 cap=2 slice=[0 1]

    // 同时添加多个元素
    numbers = append(numbers, 2, 3, 4)
    printSlice(numbers)
    //len=5 cap=6 slice=[0 1 2 3 4]
    //底层原理:
    //1.底层追加元素的时候对数组进行扩容,老数组扩容为新数组:
    //2.创建一个新数组,将老数组中的0,1复制到新数组中,在新数组中追加2, 3, 4
    //3.numbers底层数组的指向,指向的是新数组 
    //4.往往我们在使用追加的时候其实想要做的效果直接给numbers追加
    //5.但是实际上是通过切片间接操作追加的

    // 同时添加切片
    numbers1 := []int{2, 3, 4}
    numbers = append(numbers, numbers1...) // 切片后加..., 相当于拆包成单个元素
    printSlice(numbers)
    //len=8 cap=12 slice=[0 1 2 3 4 2 3 4]

    // 创建切片 numbers1 是之前切片的两倍容量
    numbers2 := make([]int, len(numbers), (cap(numbers))*2)
    // 拷贝 numbers 的内容到 numbers1
    copy(numbers2, numbers)
    printSlice(numbers2)
    //len=8 cap=24 slice=[0 1 2 3 4 2 3 4]
}
func printSlice(x []int) {
    fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行扩容,此时该切片指向的底层数组就会更换。扩容操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
举个例子:

func main() {
    //append()添加元素和切片扩容
    var numSlice []int
    for i := 0; i < 10; i++ {
        numSlice = append(numSlice, i)
        fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    }
}

c29c1392ba1434634132ffcf66fe642f.png
append()函数将元素追加到切片的最后并返回该切片。
切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍。

5.4 copy函数

由于切片是引用类型,如果直接将一个切片使用=赋值给另一个变量(注意是变量不是切片,切片间不能赋值),那么这两个其实都指向了同一块内存地址。修改其中一个变量的同时另一个变量的值也会发生变化。Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中。

5.4.1 格式

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

copy(destSlice, srcSlice []T)
//srcSlice: 数据来源切片
//destSlice: 目标切片

5.4.2 例子

package main
import "fmt"
func main() {
    // copy()复制切片
    a := []int{1, 2, 3, 4, 5}
    // c必须有内存,才能使用copy函数,不能声明完就直接使用copy,会返回一个空的切片。
    // 这里涉及到指针问题,可以到后面学完指针再来看,其原因个人感觉是因为c在声明时是声明了一个空指针,指针是只能存放地址,如果直接使用copy函数,那么切片将复制给c所指向的空的地址,这是数据就没有用了
    // 而make是申请一块地址,将地址给c,此时c就索引了一块确实存在的地址
    c := make([]int, 5, 5)
    
    copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1 2 3 4 5]
    c[0] = 1000
    fmt.Println(a) //[1 2 3 4 5]
    fmt.Println(c) //[1000 2 3 4 5]
}

5.5 delete函数

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。

5.5.1 格式

delete()函数的格式如下:

delete(map, key)
//map:表示要删除键值对的map
//key:表示要删除的键值对的键

5.5.2 例子

package main
import "fmt"
func main() {
    /* 创建map */
    countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
    fmt.Println("原始地图")
    /* 打印地图 */
    for country := range countryCapitalMap {
        fmt.Println(country, "首都是", countryCapitalMap[country])
    }
    /*删除元素*/ delete(countryCapitalMap, "France")
    fmt.Println("法国条目被删除")
    fmt.Println("删除元素后地图")
    /*打印地图*/
    for country := range countryCapitalMap {
        fmt.Println(country, "首都是", countryCapitalMap[country])
    }
}

906241cf71a8c08cf4458dc00142493a.png

5.6 Go 语言范围Range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

package main
import "fmt"
func main() {
    //这是我们使用range去求一个slice的和。使用数组跟这个很类似
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum)
    //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
    for i, num := range nums {
        if num == 3 {
            fmt.Println("index:", i)
        }
    }
    //range也可以用在map的键值对上。
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

50f223434f0bc1bd371c499abaa3bf2e.png

avatar
小C&天天

修学储能 先博后渊


今日诗句