news 2026/7/19 23:17:52

go语言基础

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
go语言基础

语法注意事项

  1. 源文件以go为扩展名
  2. 程序执行入口是main()函数
  3. 严格区分大小写
  4. 方法由一条条语句构成,每个语句后不需要分号(go语言会在每一行后面自动加分号)
  5. go编译器是由一行行进行编译的,因此一行就写条语句,不能把多条语句写在同一行
  6. 定义的变量或者import 的包如果没有使用到,代码不能编译通过
  7. 大括号都是成对出现的,缺一不可
  8. 一行最长超过80个字符,超过要换行
    fmt.Println("aaaaaaa","aaaaaaaa","aaaaaaaa")

go语言结构

go语言的基础组成

  1. 包声明
  2. 引入包
  3. 函数
  4. 变量
  5. 语句 & 表达式
  6. 注释
package main //包声明 import "fmt" //引入包 func main(){ //程序执行函数 fmt.Println("Hello World") //输出内容 }

执行go程序

在终端上执行 go run go文件

用go build 命令生成二进制文件

go语言基础语法

go标志

go程序由许多标志组成,关键字、标志符、常量、字符串、符号

行分割

go程序每一行代表一个语句,在编译的时候自动生成;号。故不用加分号

多个语句不可以在同一行,若要在同一行需要分号隔开

注释

行注释: // 快捷键: CTRL + ?

块注释:/* 内容 */

标识符

标识符用来命名变量、类型等程序实体

无效标识符:

  1. 以数字开头
  2. go语言关键字
  3. 有运算符

字符串连接

通过+号连接

fit.Println("hello"+"world")

关键字

var 定义变量 //可以省略声明类型

var a int

var b string

var c byte

const 定义常量

const a int

const b string

const c byte

go语言空格

空格常用于分割标识符、关键字、运算符、和表达式,增加代码的可读性

也可以不加空格

格式化字符串

Sprintf

生成格式化字符串后返回该字符串

var a=fmt.Sprintf("a:%d&&b:%s",1,"sss") //先格式化再写入a中 fmt.Println(a) //输出a

Printf

生成格式化字符穿后写入标准输出

fmt.Printf("a:%d&&b:%s",1,"sss") //直接输出

:= //新建变量加赋值

= //只有赋值

go语言数据类型

数据类型用于声明函数和变量

布尔型

是可以是常量true 或者 false

var b bool = true

数字类型

整数型

无符号类型

uint8 uint16 uint32 uint64

有符号类型

int8 int 16 int 32 int64

可以直接用int

浮点型

float32 float64

复数型 complex64 complex128

注意:必须标注位数

其他数字类型

byte 类似于 uint8

rune 类似于int32

int 与uint一样大

uintptr 无符号类型,用于存放一个指针

字符串类型

使用uft-8编码标识Unicode文本

派生类型

  1. 指针类型Pointer
  2. 数组类型
  3. 结构化类型struct
  4. Channel类型
  5. 函数类型
  6. 切片类型
  7. 接口类型
  8. Map类型

go语言变量

变量名由数字、下划线、字母组成,数字不能放在首位

声明变量的一般形式使用var关键字

var 变量名 变量类型

一次可以声明多个变量

var 变量1,变量2,... 变量类型

变量声明

指定变量类型,如果没有初始化,则变量默认为零值

默认值

  1. 数值类型为0
  2. 布尔类型为false
  3. 字符串为“”(空字符串)
  4. 其他为 nil

根据值自行判定变量类型

var v_name = value

使用:=声明变量

v_name := value 相当于 var v_name; v_name = value;

注意:如果变量已经使用var声明过了,在使用 := 声明变量就会产生编译错误

多变量声明

//类型相同多个变量, 非全局变量

var vname1, vname2, vname3 type

vname1, vname2, vname3 = v1, v2, v3

// 和 python 很像,不需要显示声明类型,自动推断

var vname1, vname2, vname3 = v1, v2, v3

// 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误

vname1, vname2, vname3 := v1, v2, v3

// 这种因式分解关键字的写法一般用于声明全局变量

var (

vname1 v_type1

vname2 v_type2

)

值类型与引用类型

值类型

使用等号,将一个变量的值赋值给另一个变量

引用类型

引用类型的变量储存的是变量的值所在的存储地址,或存储地址中第一个字所在的位置

这个内存地址称为指针

同一个引用类型的指针的多个字可以是在连续的内存地址中,也可以将这些字分散在内存中

引用类型赋值是将地址复制,如果被引用变量的值修改了,所有引用他的值也会被修改

go语言常量

常量是一个简单值的标识符,在程序运行是不可以修改

可以是 数字型、布尔型、字符串型

基本语法与参数

常量定义格式: const 常量名 类型 = 常量值

类型可以省去

定义方式

  1. 显示类型定义:const b string = "abc:"
  2. 隐式类型变量:const b = "abc"
  3. 批量定义:const a , b , c = v1 , v2 , v3

常量组与枚举

go语言没有枚举关键字,通常使用常量组来实现枚举效果

package main import "fmt" //定义枚举 const ( unknown = iota female male ) func main() { fmt.Printf("未知=%d,女性=%d,男性=%d", unknown, female, male) }

内置函数在使用中的应用

常量表达式中可以使用 len() , cap() , unsafe.Sizeof() 等内置函数计算表达式的值

注意:常量表达式的值必须是内置函数

iota常量计数器

可以在 const 声明块中自主生成递增的序列值

重要特性

  • iota 在 const 关键字出现时将被重置为 0
  • const 中每新增一行常量声明,iota 自动计数加一
  • const 语句块中的索引从0开始
package main import "fmt" func main() { const ( a = iota // 0 b // 1 c // 2 d = "ha" // 独立值,iota += 1 e // "ha",iota += 1 f = 100 // 独立值,iota += 1 g // 100,iota += 1 h = iota // 7,恢复使用 iota 值 i // 8 ) fmt.Printf("a=%d, b=%d, c=%d, d=%s, e=%s, f=%d, g=%d, h=%d, i=%d\n", a, b, c, d, e, f, g, h, i) }
package main import "fmt" const ( i = 1 << iota // 1 << 0 = 1,1*2^0 j = 3 << iota // 3 << 1 = 6, 3*2^1 k // 3 << 2 = 12, 3*2^2 l // 3 << 3 = 24 , 3*2^3 ) func main() { fmt.Printf("i=%d, j=%d, k=%d, l=%d\n", i, j, k, l) }

go语言运算符

1.算数运算符

自增 : ++

自减 : --

2.关系运算符

3.逻辑运算符

与:&&

或:||

非:!

4.位运算符

运算符

描述

&

参与运算的两个数对应的二进制位相与

|

参与运算的两个数对应的二进制位相或

^

参与运算的两个数对应的二进制位相异时,结果为1

<<

左移运算符,左移n位是乘以2的n次方。

是将数的二进制全部左移n位,高位丢弃,低位补0

>>

右移运算符,右移n位是除以2的n次方。

是将数的二进制全部右移n位,低位丢弃,高位补0

5.赋值运算符

= 简单的赋值运算

运算后赋值的有:+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=

6.其他运算符

& 返回变量存储地址 //&a 将给出变量的实际地址

*指针变量 //*a 是一个指针变量

7.运算符优先级

优先级

运算符

5

*/ % << >> & &^

4

+- | ^

3

== != < <= > >=

2

&&

1

||

通过使用括号来临时提升某个表达式的整体运算优先级

go语言条件语句

if语句

if else 语句

go特有

if里面可以先定义变量,变量仅在if/else里有效

if num := 20; num > 10 { fmt.Println("大于10") } else { fmt.Println("小于等于10") }

if 嵌套语句

switch 语句

基础特点

  1. go 的 switch 自带 break,匹配后自动结束,不用手写break
  2. 支持多条件匹配无表达式 、无表达式、fallthrough 穿透
  3. case 可以放表达式、变量、常量

基础语法

switch 变量 { case 值1: 逻辑1 case 值2, 值3: // 多个条件满足其一就匹配 逻辑2 default: // 都不匹配执行 默认逻辑 }

无表达式 switch

类似于 if-else 多判断

switch 后不写变量,等价于 switch true。 case 里写布尔判断

case中初始化变量

switch 开头可以定义临时变量

fallthrough 穿透

go 默认不穿透,fallthrough 会强制执行下一个case

select 语句

select 语句类似于 switch 语句, 但是select会随机执行一个可运行的case。如果case可运行,

他将会阻塞,直到有case可运行

基础语法

select { case 通道读操作: // 读就绪执行 case 通道写操作: // 写就绪执行 default: // 所有通道都未就绪时执行(非阻塞) }

有default 时select 可能会发生阻塞

没有default 时select 不会发生阻塞

超时控制

使用 time.After 防止 select永久阻塞

package main import ( "fmt" "time" ) func main() { ch := make(chan string) select { case res := <-ch: fmt.Println(res) case <-time.After(2 * time.Second): fmt.Println("等待2秒超时") } }

go语言循环语句

循环类型

for 循环

循环嵌套

循环控制语句

控制语句

描述

break语句

终断当前的for循环或跳出switch语句

continue语句

跳出当前循环的剩余语句,然后继续进行下一个循环

goto 语句

将控制转移到被标记的语句

goto语句

  1. 基础定义
    无条件跳转语句,直接跳转到同函数内只定义的标签(label)所在代码行,直线控制流转移
package main import "fmt" func main() { i := 0 LOOP: // 定义标签 fmt.Println(i) i++ if i < 3 { goto LOOP // 跳转到LOOP标签 } fmt.Println("结束") }
  1. 关键约束
    1.只能在同一个函数里跳转,不能跳转到别的函数
    2.不允许跳过变量声明
    3.不能从代码外层跳进 if 、 for 等代码块内部
    4.允许从循环/判断内部,跳出到外部标签
  2. 实用的应用场景
    break 只能跳出当前的一层循环
    goto 可直接终止所有的嵌套循环

无限循环

package main import "fmt" func main(){ for true{ fmt.Pritf("这是无限循环") } }

go语言函数

函数是基本的代码块,用于执行一个任务

go语言至少有个main函数

函数的声明需要告诉编译器函数的名字,返回类型,和参数

go语言标准库有许多内置函数

函数的定义

go语言函数格式

func function_name( [parameter list] ) [reture_types ] {

函数体

}

  • func : 函数由func 开始声明
  • function_name:函数名称,参数列表和返回值构造了成了函数签名
  • paramater_list :参数列表
  • return_types: 返回值类型 //有些函数不需要返回值,不是必须的
  • 函数体:函数定义的代码聚合
package main import "fmt" func max1(a, b int) int { if a > b { return a } else { return b } } func main() { var maxn int maxn = max1(1, 2) fmt.Println(maxn) }

函数调用

调用函数时,向函数传递参数,并返回值

同一个go文件内

函数的调用无先后顺序

同一个包下跨文件调用

同一个包下的不同文件下,无需前置声明,可以直接调用

函数首字母权限:

  • 首字母小写:包私有,进当前包所有文件可调用
  • 首字母大写:导函数,当前包,外部其他包均可以调用

挎包调用

满足三个条件:

  • 被调用包的函数名首字母大写
  • 调用函数的文件要导入目标包 // import "项目下包的路径"
  • 调用格式: 包名.函数名()

函数参数

函数使用的参数,该变量称为函数形参

形参就像是定义在函数内的局部变量

值传递

在调用函数时,将实际参数复制一份传递到函数上,在函数中对参数修改,不过会影响实际参数

引用传递

核心定义

引用传递本质:吧实参的内存地址传递给函数形参

函数内通过地址修改内存里的数据,原始实参的值会用不发生改变

两个关键符号
  1. &变量: 去变量内存地址
  2. *指针变量: 解引用,操作地址对应的的值
package main import "fmt" // 参数是 int 指针,接收地址 func changeNum(n *int) { *n = 999 // 通过地址修改原始变量的值 } func main() { a := 10 changeNum(&a) // &a 取出a的地址传入函数 fmt.Println(a) // 输出 999,原值被修改 }

函数用法

作为另一个函数的实参

go中的函数可以像普通函数一样:定义、赋值、当作参数传递给其他函数、作为返回值返回

把一个函数传递入另一个函数的参数位置,就是“函数做实参”

  1. 先定义函数类型
    格式: type 类型名 func(参数类型) 返回值类型
// 定义函数类型 calc // 规则:必须接收2个int参数,返回1个int type calc func(int, int) int
  1. 目标函数声明该类型参数
// compute就是目标函数,第三个参数f的类型是calc func compute(a, b int, f calc) int { return f(a, b) } //f calc: 代表参数f 只能满足calc格式的函数
  1. 调用时满足格式的函数传出去
    前提要有一个完全匹配calc模板的普通函数
    再把add直接当作第三个实参传递给目标函数
package main import "fmt" // 第一步:定义函数类型,定好函数格式标准 type calc func(int, int) int // 第二步:目标函数,声明calc类型的形参f func compute(a, b int, f calc) int { return f(a, b) } // 符合calc格式的普通函数 func add(x, y int) int { return x + y } func main() { // 第三步:调用,传入满足格式的add函数 res := compute(2, 3, add) fmt.Println(res) }

闭包与匿名函数

匿名函数

没有函数名的函数就叫匿名函数

// 格式1:定义后立刻调用 func(参数) 返回值 { 逻辑 }(传入实参) // 格式2:赋值给变量保存 变量名 := func(参数) 返回值 { 逻辑 }

闭包

定义:捕获了外层局部变量的匿名函数

两个必要条件:

  1. 本身时匿名函数
  2. 匿名函数能访问、修改外层函数的局部变量,延长变量生名命周期

核心特点:闭包会记住外层变量的环境,哪怕外层函数执行完毕,变量也不会销毁

闭包格式

// 外层函数,返回值为函数类型 func 外层函数名() func(形参) 返回类型 { //闭包类型的返回值类型: func(形参) 返回值类型 // 外层局部变量(被闭包捕获) 变量 := 初始值 // 返回匿名函数(闭包) return func(形参) 返回类型 { // 操作外层变量 变量++ return 变量 } }
package main import "fmt" // 外层函数,返回一个闭包 func counter() func() int { num := 0 // 外层局部变量 // 返回匿名函数(闭包,捕获num) return func() int { num++ //使用并修改了匿名函数外的局部变量 return num } } func main() { f := counter() fmt.Println(f()) // 1 fmt.Println(f()) // 2 fmt.Println(f()) // 3 f2 := counter() fmt.Println(f2()) // 1(全新独立变量num) }

方法

定义:本质是绑定了接受者的函数

格式

func (接收者变量 接收者类型) 方法名 (参数列表) 返回值列表{

方法逻辑

}

接收者:代表这个方法属于哪一种自定义类型(结构体,基础自定义类型)

调用方式:变量.方法名

接受方式

  1. 值接收
package main import "fmt" type Person struct { name string age int } // 值接收者 func (p Person) rename(newName string) { p.name = newName // 修改的是拷贝出来的副本 } func main() { man := Person{name: "小明", age: 18} man.rename("小红") fmt.Println(man.name) // 输出:小明,原值没变 }
  1. 指针接收
// 指针接收者 func (p *Person) grow() { p.age++ // 直接修改原结构体 } func main() { man := Person{name: "小明", age: 18} man.grow() fmt.Println(man.age) // 输出:19,原值被修改 }

go语言变量作用域

局部变量

在函数体内声明的变量称为局部变量,只作用于函数体内

参数和返回值变量也是局部变量

全局变量

在函数体外面的变量称为全局变量,

全局变量可以在整个包使用

注意:全局变量与局部变量名字是相同的,但是函数内部的变量会优先考虑

go语言数组

声明数组

格式:var arrayName [size]dataType

初始化数组

默认初始化

var numbers [5]int //默认初始值都为0

var numbers = [5]int{1,2,3,4,5}

numbers := [5]int{1,2,3,4,5}

注意:数组的大小是类型的一部分,因此不同大小的数组是不兼容的

如果数组的长度不确定,可以使用...来代替长度,编译器会根据元素的个数判断长度

访问数组元素

通过循环遍历数组元素

go语言指针

指针变量指向的是一个值的变量地址

指针:存储变量内存地址的变量

两个关键符号
  1. &变量: 取变量内存地址
  2. *指针变量: 解引用,操作地址对应的的值

指针使用流程

定义指针变量

使用前需要声明指针,格式:

var var_name *var-tpye

  1. var_name 为指针变量名
  2. *用于指定变量是作为一个指针
  3. var-type 为指针类型 *int指向整数型 *float32 指向浮点型

为指针变量赋值

p = &num

&变量:取变量内存地址,把地址存进指针 p

访问指针变量的值

*指针变量:根据指针存放的地址,取出对应对应内存里的数据

package main import "fmt" func main() { var a int= 20 /* 声明实际变量 */ var ip *int /* 声明指针变量 */ ip = &a /* 指针变量的存储地址 */ fmt.Printf("a 变量的地址是: %x\n", &a ) /* 指针变量的存储地址 */ fmt.Printf("ip 变量储存的指针地址: %x\n", ip ) /* 使用指针访问值 */ fmt.Printf("*ip 变量的值: %d\n", *ip ) }

空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil

nil 的指针也为空指针

nil 指的是零值或空值

指针数组

概念:数组中每个元素都是指针,统一类型 *T

package main import "fmt" func main() { a, b, c := 10, 20, 30 // 定义指针数组,3个*int元素 var ptrArr [3]*int = [3]*int{&a, &b, &c} // 遍历打印地址、解引用值 for i := 0; i < 3; i++ { fmt.Printf("地址:%p,值:%d\n", ptrArr[i], *ptrArr[i]) } // 通过指针修改原变量 *ptrArr[1] = 999 fmt.Println(b) // 999 }

二级指针

指向指针的指针

普通变量->一级指针存放变量地址->二级指针存放一级指针地址

语法: var pp **int

package main import "fmt" func main() { num := 66 var p *int = &num // 一级指针 var pp **int = &p // 二级指针,存p的地址 fmt.Println("num原值:", num) fmt.Println("p存储地址:", p) fmt.Println("pp存储p的地址:", pp) fmt.Println("*pp = p:", *pp) fmt.Println("**pp = num:", **pp) // 二级指针修改原始值 **pp = 888 fmt.Println(num) // 888 }

函数传递指针参数

值传递:函数内修改只是副本,外部不变

指针传递:传变量地址函数内解引用直接修改外部原始数据

package main import "fmt" // 接收int指针 func changeVal(x *int) { *x = 1000 // 修改原变量 } func main() { n := 10 changeVal(&n) // 传入地址 fmt.Println(n) // 1000 }

go语言结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据的 集合

定义结构体

type 结构体名字 struct { meber definition meber definition meber definition ... }

定义结构体后就可以用于结构声明,格式如下

variable_name := 结构体名字 {value1,value2,value3...} variable_name := 结构体名字 { key1: value1, key2 : value2, key3 : value3 }

访问结构体成员

格式为:结构体.成员名

package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* 声明 Book1 为 Books 类型 */ var Book2 Books /* 声明 Book2 为 Books 类型 */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.runoob.com" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* book 2 描述 */ Book2.title = "Python 教程" Book2.author = "www.runoob.com" Book2.subject = "Python 语言教程" Book2.book_id = 6495700 /* 打印 Book1 信息 */ fmt.Printf( "Book 1 title : %s\n", Book1.title) fmt.Printf( "Book 1 author : %s\n", Book1.author) fmt.Printf( "Book 1 subject : %s\n", Book1.subject) fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id) /* 打印 Book2 信息 */ fmt.Printf( "Book 2 title : %s\n", Book2.title) fmt.Printf( "Book 2 author : %s\n", Book2.author) fmt.Printf( "Book 2 subject : %s\n", Book2.subject) fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id) }

结构体作为函数参数

可以将结构体类型作为参数传递给函数,

package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* 声明 Book1 为 Books 类型 */ var Book2 Books /* 声明 Book2 为 Books 类型 */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.runoob.com" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* book 2 描述 */ Book2.title = "Python 教程" Book2.author = "www.runoob.com" Book2.subject = "Python 语言教程" Book2.book_id = 6495700 /* 打印 Book1 信息 */ printBook(Book1) /* 打印 Book2 信息 */ printBook(Book2) } func printBook( book Books ) { fmt.Printf( "Book title : %s\n", book.title) fmt.Printf( "Book author : %s\n", book.author) fmt.Printf( "Book subject : %s\n", book.subject) fmt.Printf( "Book book_id : %d\n", book.book_id) }

结构体指针

可以定义结构体的指针类似与其他指针变量

var struct_pointer *Books

package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* 声明 Book1 为 Books 类型 */ var Book2 Books /* 声明 Book2 为 Books 类型 */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.runoob.com" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* book 2 描述 */ Book2.title = "Python 教程" Book2.author = "www.runoob.com" Book2.subject = "Python 语言教程" Book2.book_id = 6495700 /* 打印 Book1 信息 */ printBook(&Book1) /* 打印 Book2 信息 */ printBook(&Book2) } func printBook( book *Books ) { fmt.Printf( "Book title : %s\n", book.title) fmt.Printf( "Book author : %s\n", book.author) fmt.Printf( "Book subject : %s\n", book.subject) fmt.Printf( "Book book_id : %d\n", book.book_id) }

go语言切片

切片 slice 十动态数组,基于数组封装,长度可变,底层引用数组

go语言切片是对数组的抽象,是动态数组

切片的长度是不固定的,可以追加元素,在追加时可能是切片的容量增大

定义切片

  1. 可以使用为指定大小的数组来定义切片
    var identifier []type
  2. 使用 make()函数来创建切片
    var slice1 []type =make([]type, len)
    slice1 : make([]type, len)
  3. 可以指定容量,其中 capacity 为可选参数
    make([]type,len,capacity)

这里 len 是数组的长度也是原始长度,capacity 为切片容量

切片初始化

arr := []int {1,2,3,4}

切片: s:= arr[start : end : max] //arr[start : end]为左开右闭

len 与 cap 函数

切片是可以索引的,并且可以由len()方法获取长度

cap()方法可以计算容量

append 和 copy 函数

append为追加函数

nums = append( nums , a , b, c) //在nums后面追加a,b,c

copy 为拷贝函数

copy ( nums1 , nums2 ) //拷贝 nums2的内容到nums1

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/19 23:16:47

React Photo Album性能优化指南:从基础到高级的提速技巧

React Photo Album性能优化指南&#xff1a;从基础到高级的提速技巧 【免费下载链接】react-photo-album Responsive photo gallery component for React 项目地址: https://gitcode.com/gh_mirrors/re/react-photo-album React Photo Album是一个功能强大的响应式照片画…

作者头像 李华
网站建设 2026/7/19 23:15:32

揭秘QobuzDownloaderX-MOD:音乐流解析与下载原理的技术实现

揭秘QobuzDownloaderX-MOD&#xff1a;音乐流解析与下载原理的技术实现 【免费下载链接】QobuzDownloaderX-MOD Downloads streams directly from Qobuz. Experimental refactoring of QobuzDownloaderX by AiiR 项目地址: https://gitcode.com/gh_mirrors/qo/QobuzDownloade…

作者头像 李华
网站建设 2026/7/19 23:13:43

2026公益志愿者招募管理小程序开发提案

2026公益志愿者招募管理小程序开发提案&#xff1a;从指尖连接到心与心 公益事业在数字化浪潮中迎来了全新的变革节点。根据最新发布的《中国志愿服务发展报告》显示&#xff0c;2025年全国实名注册志愿者已突破2.4亿人&#xff0c;年均志愿服务时长超过42亿小时。然而&#xf…

作者头像 李华