您的位置:首页 >视频 >

从一道面试题来谈谈Golang中的 ==

2023-08-05 13:38:37    来源:博客园

写这篇文章的时候,已经离我找工作有一段时间了,但是觉得这道题不管是面试还是日常的工作中,都会经常遇到,所以还是特意写一篇文章,记录下自己对Golang中==的理解。如文章中出现不对的地方,请不吝赐教,谢谢。

注意,以下文章内容是基于 go1.16.4 进行演示的,如果和你验证时,结果不一致,可能 Go 的判断规则有所改变。


(资料图)

1、面试题

大家可以先不看结果,想想答案,再看后面的结果以及相关的分析。

type T interface {}func main()  {var (t Tp *Ti1 interface{} = ti2 interface{} = p)fmt.Println(i1 ==t, i1 == nil)fmt.Println(i2 ==p, i2 == nil)fmt.Println(t == nil)fmt.Println(p == nil)}

执行结果:

true truetrue falsetruetrue

分析:

1、interface 值由动态类型动态值组成。只有在类型都相同时才相等。接口变量i1是接口类型的零值,也就是它的类型和值部分都是nil,接口变量i2的动态值虽然是零值,但是动态类型为 *T。2、变量 t、p 都没有初始化,未分配内存,所以 变量t、p 都等于 nil。

对于上面的描述不太清楚的同学,不用着急,我们一起来学习 Golang 中的==,有较为详细的介绍。

2、Golang中的数据类型

Golang中的数据类型分为4大类,他们分别是:

基本类型 (Primary types):整型(int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune等)、浮点数(float32/float64)、复数类型(complex64/complex128)、字符串(string)、布尔(true/false)。这些是Go语言内置的基本数据类型,它们是Go语言的原始数据类型,不能再细分。复合类型 (Composite types):又叫聚合类型。包括数组、结构体。复合类型允许将多个值组合成一个新的数据结构。引用类型 (Reference types):这些类型在内存中存储的是数据的地址,包括 指针、切片(slice)、映射(map)、通道(channel)、函数类型(func)。引用类型允许在函数间共享和修改数据。接口类型 (Interface types):接口类型是一种抽象类型,它定义了对象的行为,而不关心对象的具体类型。通过实现接口,可以实现多态性和代码复用。比如 error

其实接口类型可以看作是引用类型,在 Go 中,接口类型是一种特殊的引用类型,它包含一个指向实际数据的指针以及类型信息。当你将一个具体类型的值赋给接口变量时,接口会存储一个指向实际数据的指针或实际数据的拷贝。因此,接口可以看作是对其他类型的引用,而不是直接包含实际数据。

在Go语言中,自定义类型属于基本类型的概念中。

自定义类型属于基本类型的一种,它通过使用 type 关键字来创建新的类型,底层使用基本数据类型。通过自定义类型,我们可以为基本类型赋予更多语义,并且可以为它们定义自己的方法。自定义类型和其他基本类型具有相同的操作和运算规则,但在类型系统中它们是不同的类型。

例如使用 type number int64时,我们自定义了一种数据类型,叫做number。虽然它底层使用了int64,但在类型系统中,numberfloat64是不同的类型。

在Go语言中,还有一种类型别名的叫法,是 Go1.9 引用的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。例如:

type byte = uint8type rune = int32

==操作最重要的一个前提是:两个操作数类型必须相同!!!

golang 的类型系统非常严格,没有C/C++/python中的隐式类型转换。这个需要注意。

3、四大类型如何使用 ==3.1、基本类型

基本类型的比较,就比较简单直观,直接使用==判断就好了,注意的是Go中并没有隐式转换,而且类型一致才可以

package mainimport "fmt"func main() {var a int64var b int64var c int32fmt.Println(a == b)fmt.Println(c)// Invalid operation: a == c (mismatched types int64 and int32)//fmt.Println(a == c)}

接下来我们看看浮点数的比较:

package mainimport "fmt"func main() {var a float64 = 0.1var b float64 = 0.2var c float64 = 0.3fmt.Println(a + b)  // 0.30000000000000004fmt.Println(a+b == c)  // false}

是不是有点小惊讶,这个是因为Go 中的 浮点数遵循 IEEE 754 标准,所以会有有些浮点数不能精确表示,浮点运算结果会有误差。

想大概了解计算机是如何表示浮点数的可以看看下面的文章,有一个基础的了解。

数字编码

注意:

浮点数做 判等 操作一般是使用 计算两个浮点数的差的绝对值,如果小于一定的值就认为它们相等,比如1e-9

package mainimport ("fmt""math")func main() {var a = 0.1var b = 0.2var c = 0.3fmt.Println(a + b)  // 0.30000000000000004fmt.Println(math.Abs((a+b)-c) < 1e-9) // truefmt.Printf("%T", a) // float64}
3.2、复合类型

合类型也叫做聚合类型。golang 中的复合类型只有两种:数组和结构体。它们是逐元素/字段比较的。

注意:数组的长度视为类型的一部分,长度不同的两个数组是不同的类型,不能直接比较

对于数组来说,依次比较各个元素的值。根据元素类型的不同,再依据是基本类型、复合类型、引用类型或接口类型,按照特定类型的规则进行比较。所有元素全都相等,数组才是相等的。对于结构体来说,依次比较各个字段的值。根据字段类型的不同,再依据是 4 中类型中的哪一种,按照特定类型的规则进行比较。所有字段全都相等,结构体才是相等的。

注意:如包含了不支持直接使用 == 符号的类型,在编译阶段会报错。

例如:

package mainimport "fmt"type Student struct {Name stringAge  intSex  bool}type S1 struct {Name   stringScores []int8  // 注意这里定义的是 slice 类型}type ITest interface{}func main() {arrayA := [...]int64{2, 3, 4}arrayB := [...]int64{2, 3, 4}arrayC := [...]int64{1, 3, 4}fmt.Println(arrayA == arrayB) // truefmt.Println(arrayB == arrayC) // falsefmt.Println("-------")s1 := Student{"xiaoming", 18, false}s2 := Student{"xiaoming", 18, false}s3 := Student{"xiaowang", 18, false}fmt.Println(s1 == s2) // truefmt.Println(s1 == s3) // falsefmt.Println("-------")a1 := [...]Student{s1, s2}// 注意这两个元素!a2 := [2]Student{s2, s2}a3 := [2]Student{s2, s3}fmt.Println(a1 == a2) // truefmt.Println(a3 == a2) // falsefmt.Println("-------")var i1 ITest = 23var i2 ITest = 23var i3 ITest = "tt"var i4 ITest = 23fmt.Println(i1 == i2) // truefmt.Println(i3 == i4) // falseis1 := [...]ITest{i1, i2}is2 := [...]ITest{i1, i4}is3 := [...]ITest{i1, i3}fmt.Println(is1 == is2) // truefmt.Println(is1 == is3) // falsefmt.Println("-------")t1 := S1{"xw", []int8{66, 88}}t2 := S1{"xw", []int8{66, 88}}t3 := S1{"xw", []int8{66, 99}}  // 为什么这里会报错呢,因为我们定义的结构体中的 Score 字段是 slice, slice 是不支持使用 == 符号的// Invalid operation: t1 == t2 (the operator == is not defined on S1)//fmt.Println(t1 == t2)// Invalid operation: t1 == t2 (the operator == is not defined on S1)//fmt.Println(t1 == t3)// go 中 slice 使用 reflect.DeepEqual 判断是否相等fmt.Println(reflect.DeepEqual(t1, t2)) // truefmt.Println(reflect.DeepEqual(t1, t3)) // false}
3.3、引用类型

引用类型是指那些底层数据结构的值是引用地址(指针)的类型。它们在内存中存储的是指向实际数据的指针,而不是实际数据本身。切片、映射、通道和函数都是引用类型,因为它们在底层都使用了指针来引用实际的数据。

引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。

关于引用类型,有几个比较特殊的规定:

切片之间不允许比较。切片只能与nil值比较。map之间不允许比较。map只能与nil值比较。函数之间不允许比较。函数只能与nil值比较。

接下来我们在仔细看看各个类型的具体介绍。

3.3.1、指针
package mainimport ("fmt")type Student struct {Name stringAge  intSex  bool}func main() {s1 := &Student{"xiaoming", 18, false}s2 := &Student{"xiaoming", 18, false}s3 := s1fmt.Println(s1 == s2) // falsefmt.Println(s1 == s3) // true}

s1 和 s2 虽然数据一样,但是他们在内存中的地址并不相等,所以他们是不相等的,s1 和 s3 指向的是同一份内存地址,所以是相等的。

3.3.2、channel 和 函数类型

接下来我们再看看 channel 和 函数类型:

package mainimport "fmt"type Student struct {Name stringAge  intSex  bool}func main() {ch1 := make(chan bool, 1)ch2 := make(chan bool, 1)ch3 := ch1fmt.Println(ch1 == ch2) // falsefmt.Println(ch1 == ch3) // truefmt.Println("-----")a := TestFuncb := TestFuncc := a// invalid operation: a == b (func can only be compared to nil)//fmt.Println(a == b)// invalid operation: a == c (func can only be compared to nil)//fmt.Println(a == c)fmt.Println(a)  // 0x10a3400fmt.Println(b)  // 0x10a3400fmt.Println(c)  // 0x10a3400}func TestFunc() {}

从上面可以看出来,函数类型不支持直接判等操作。原因是:函数类型不支持直接的判等操作是因为函数类型是一种复杂的类型,它包含了函数的签名和实现代码等信息。由于函数可以是闭包,可能捕获了外部变量,因此函数的判等操作会涉及到比较函数的底层实现和捕获的变量等细节,这会导致判等操作的复杂性和不确定性。

所以从中也可以看出来 Go 中判断引用类型是否相等,不是简单的判断变量所在的内存地址是否一致,而是根据相应的类型,有不同的判断规则,这里大家需要注意。

3.3.3、slice

再看看切片。因为切片是引用类型,它可以间接的指向自己。例如:

a := []interface{}{ 1, 2.0 }a[1] = afmt.Println(a)// !!!// runtime: goroutine stack exceeds 1000000000-byte limit// fatal error: stack overflow

上面代码将a赋值给a[1]导致递归引用,fmt.Println(a)语句直接爆栈。

切片如果直接比较引用地址,是不合适的。首先,切片与数组是比较相近的类型,比较方式的差异会造成使用者的混淆。另外,长度和容量是切片类型的一部分,不同长度和容量的切片如何比较?切片如果像数组那样比较里面的元素,又会出现上来提到的循环引用的问题。虽然可以在语言层面解决这个问题,但是 golang 团队认为不值得为此耗费精力。

基于上面两点原因,golang 直接规定切片类型不可比较。使用==比较切片直接编译报错。

例如:

var a []intvar b []int// invalid operation: a == b (slice can only be compared to nil)fmt.Println(a == b)

如果实在是需要判断 slice 中元素是否相等,我们一般是自定义一个 判断函数或者使用reflect.DeepEqual函数。

package mainimport ("fmt""reflect")func slicesAreEqual(slice1, slice2 []int) bool {if len(slice1) != len(slice2) {return false}for i := 0; i < len(slice1); i++ {if slice1[i] != slice2[i] {return false}}return true}func main() {slice1 := []int{1, 2, 3}slice2 := []int{1, 2, 3}slice3 := []int{4, 5, 6}fmt.Println(reflect.DeepEqual(slice1, slice2)) // 输出: false (reflect.DeepEqual 可以进行值相等判断)fmt.Println(slicesAreEqual(slice1, slice2))   // 输出: truefmt.Println(slicesAreEqual(slice1, slice3)) // 输出: false}

注意,在上面的示例中,我们自定义了slicesAreEqual函数来判断两个切片是否拥有相同的元素。这个示例中我们使用了reflect.DeepEqual来进行值相等的判断,但是不推荐在切片的值相等判断中使用reflect.DeepEqual,因为它会将切片的元素逐个进行深度比较,效率较低,尤其在切片较大时。通常最好手动遍历比较切片的元素。

3.3.4、map

在 Go 中,map类型不支持直接的判等操作是因为 map是一个引用类型,并不存储在变量中的实际数据,而是一个指向底层数据结构的指针。map是一种哈希表的实现,它包含了键值对的集合。

在 Go 中,map是一个引用类型,类似于切片、通道和函数等。当你将一个 map赋值给另一个变量时,它们引用同一个底层的 map数据。因此,两个 map可能引用相同的底层数据,但它们仍然是不同的 map对象。直接比较两个 map是否相等,并不能确定它们是否引用同一个底层数据。

如果你需要判断两个 map是否包含相同的键值对,你可以通过手动遍历比较 map的键值对来实现。这涉及到比较每个键值对的键和值是否相等。

如果实在是需要判断两个map是否相等,我们可以使用自定义函数来判断两个 map是否包含相同的键值对的示例:

package mainimport ("fmt""reflect")func mapsAreEqual(map1, map2 map[string]int) bool {if len(map1) != len(map2) {return false}for key, value := range map1 {if map2Value, ok := map2[key]; !ok || map2Value != value {return false}}return true}func main() {map1 := map[string]int{"a": 1, "b": 2, "c": 3}map2 := map[string]int{"a": 1, "b": 2, "c": 3}map3 := map[string]int{"a": 1, "b": 2, "c": 4}fmt.Println(reflect.DeepEqual(map1, map2)) // 输出: false (reflect.DeepEqual 可以进行值相等判断)fmt.Println(mapsAreEqual(map1, map2))     // 输出: truefmt.Println(mapsAreEqual(map1, map3)) // 输出: false}

在上面的示例中,我们自定义了mapsAreEqual函数来判断两个 map是否包含相同的键值对。请注意,与前面提到的reflect.DeepEqual一样,我们也不推荐在 map的值相等判断中使用reflect.DeepEqual,因为它会将 map的键值对逐个进行深度比较,效率较低,尤其在 map较大时。通常最好手动遍历比较 map的键值对。

3.4、接口类型

以下内容来自后面的参考链接 深入理解Go之== ,十分感谢原博文作者。

接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

接口值的比较涉及这两部分的比较,只有当动态类型完全相同且动态值相等(动态值使用==比较),两个接口值才是相等的。

package mainimport "fmt"func main() {var a interface{} = 1var b interface{} = 2var c interface{} = 1var d interface{} = 1.0fmt.Println(a == b) // falsefmt.Println(a == c) // truefmt.Println(a == d) // false}

ab动态类型相同(都是int),动态值也相同(都是1,基本类型比较),故两者相等。 ac动态类型相同,动态值不等(分别为12,基本类型比较),故两者不等。 ad动态类型不同,aintdfloat64,故两者不等。

package mainimport "fmt"func main() {type A struct {a intb string}var aa interface{} = A{a: 1, b: "test"}var bb interface{} = A{a: 1, b: "test"}var cc interface{} = A{a: 2, b: "test"}fmt.Println(aa == bb) // truefmt.Println(aa == cc) // falsevar dd interface{} = &A{a: 1, b: "test"}var ee interface{} = &A{a: 1, b: "test"}fmt.Println(dd == ee) // false}

aabb动态类型相同(都是A),动态值也相同(结构体A,见上面复合类型的比较规则),故两者相等。 aacc动态类型相同,动态值不同,故两者不等。 ddee动态类型相同(都是*A),动态值使用指针(引用)类型的比较,由于不是指向同一个地址,故不等。

注意:

如果接口的动态值不可比较,强行比较会panic!!!

var a interface{} = []int{1, 2, 3, 4}var b interface{} = []int{1, 2, 3, 4}// panic: runtime error: comparing uncomparable type []intfmt.Println(a == b)

ab的动态值是切片类型,而切片类型不可比较,所以a == bpanic

接口值的比较不要求接口类型(注意不是动态类型)完全相同,只要一个接口可以转化为另一个就可以比较。例如:

package mainimport ("fmt""io""os")func main() {var f *os.Filevar r io.Reader = fvar rc io.ReadCloser = ffmt.Println(r == rc) // truevar w io.Writer = f// invalid operation: r == w (mismatched types io.Reader and io.Writer)fmt.Println(r == w)}
type ReadCloser interface {ReaderCloser}

r的类型为io.Reader接口,rc的类型为io.ReadCloser接口。查看源码,io.ReadCloser的定义如下:

io.ReadCloser可转化为io.Reader,故两者可比较。

io.Writer不可转化为io.Reader,编译报错。

4、注意事项

不可比较性:

前面说过,golang 中的切片类型、map类型、函数类型(func)是不可比较的。所有含有切片的类型都是不可比较的。例如:

数组元素是切片类型、map类型、函数类型(func)。结构体有切片类型、map类型、函数类型(func)的字段。指针指向的是切片类型、map类型、函数类型(func)。

不可比较性会传递,如果一个结构体由于含有切片字段不可比较,那么将它作为元素的数组不可比较,将它作为字段类型的结构体不可比较

package mainimport "fmt"func main() {type T struct {a map[string]bool}t1 := T{a: map[string]bool{"ni": true},}t2 := T{a: map[string]bool{"ni": true},}// invalid operation: t1 == t2 (struct containing map[string]bool cannot be compared)fmt.Println(t1 == t2)type T1 struct {a func()}t3 := T1{a: func() {},}t4 := T1{a: func() {},}// invalid operation: t1 == t2 (struct containing func() cannot be compared)fmt.Println(t3 == t4)}

关于引用类型,有几个比较特殊的规定:

切片之间不允许比较。切片只能与nil值比较。map之间不允许比较。map只能与nil值比较。函数之间不允许比较。函数只能与nil值比较。

参考链接:

Go语言基础之接口

4、interface

深入理解Go之==

标签:

相关推荐

《逐梦》第七集:《勇当军事变革的先锋》

全国多地仍在为方舱医院建设招标,部分中标公司成立不到一年

小米12S目前的性价比怎么样?还值得入手吗?

渣叔急了?利物浦四后卫被点名批评 防线指望不上只能靠前锋

漫评|青春与体育相遇 拼搏逐梦向未来

“小青椒”成都大运会赛场的靓丽风景线

(成都大运会)大运会“交友记”:有朋自四海来

成都大运会 | 大运会参赛代表团在成都圆“大熊猫梦”

《逐梦》第七集:《勇当军事变革的先锋》

多浦乐披露招股书拟于近期在深市发行新股并上市

李子园业绩快报:上半年净利润同比增长30.56%

昔日年销售千亿,今日黯然退市!连吃26个跌停,股价仅剩3毛7

经济出现向上拐点的几个证据

聚焦|一汽-大众将停产油车产线?内部人士:并未收到相关信息

起个大早赶个晚集,奇瑞eQ7本月上市,果断扔掉意大利设计

将两家中国企业送进世界500强:他把涨薪的气球,挂到厂区上空

漫评|青春与体育相遇 拼搏逐梦向未来

K396次列车员赵阳撰文:“爱哭包”这次咋没哭

住房城乡建设部成立工作组赴河北涿州指导防涝供水等工作

河北涿州救灾保障物资陆续到位 群众生活稳定

新华全媒+丨记者直击:河北多地消防救援力量驰援涿州

楚雄永仁中和镇:悠悠夏日美如画

亚洲7月原油进口量创新高!中印接下来还会继续“爆买”吗?

万达王健林的文旅野心再度显露 “世纪并购”6年后卷土重来

南京接力郑州,发布八条新政稳楼市

中华联合财险数家重庆分支机构及个人合计被罚541万元,多名责任人遭数次警告

未来数据集团(08229.HK)预计上半年亏损不少于680万港元 第二季度溢利约450万港元

乌反攻效果不佳,英国防部称灌木“疯长”阻碍推进 乌官员:俄前线布雷太“疯狂”

绿色生活完美杂色榕树护理指南与绝佳儿子生日礼物选择

旅游业反弹 英国航空公司大幅加薪

南非总统府为女足争取同工同酬待遇发声

泰国一火车与皮卡车相撞致8死3伤

全球海运业陷入严重海员短缺

财政部 水利部紧急下达4.5亿元中央财政水利救灾资金 支持京津冀地区做好水毁水利工程设...

官方通报重庆事业单位招考作弊案:已抓获组织作弊团伙成员

宁夏贺兰山东麓产区组团亮相武汉“中部酒博会”

三安光电上半年净利降超81%,计提减值3.6亿元

维护房地产市场平稳运行,央行上海总部部署下半年重点工作

正邦转债进入最后交易日 一周两只转债退市摘牌

印度建厂,难倒王传福

今日澳元/美元汇率走势图分析及操作建议(2020/12/1)

一汽-大众与零跑汽车谈合作,捷达品牌或主攻经济型电动车市场

科学家发现抑制疟疾传播“新武器”

“伊斯兰国”证实最高头目死亡并确定继任者

台风“卡努”影响持续 自然资源部发布风暴潮、海浪双橙警报

特朗普出庭之际,共和党人公布对拜登之子调查情况

降雨趋于结束 海河流域上游主要河道水位平稳回落

喜力在华业务被华润收购五年之际:上半年整体业绩低于预期

凯添燃气将于8月9日解禁14.15万股

海口市龙华区人民政府办公室四级调研员曾德裕被查

《富爸爸穷爸爸》作者警报升级:市场将“紧急着陆”

百济神州:核心产品收入同比大增,侵权诉讼阴霾仍在

“第二支箭”扩容 民营房企融资机会来了?

驰援京津冀防汛抢险,阿维塔科技捐赠350万元

草原生态产品成“香饽饽”,产业结构、产品供给、创新能力仍存不足

鸿蒙4.0正式揭晓,华为“曲线造车”进入下半场

德力股份拟定增股份募资不超6亿元

今晚就出发!来金山这里看一场浪漫星球灯光秀吧!(含福利)

邢台市襄都区税务局持续优化纳税服务为小微企业和个体工商户“减负增力”

海外网评:美国打开了司法政治化的“潘多拉魔盒”

安迪苏上半年净利润为3340万,同比减少96.16%

2023年暑假广州“寻梦华工研学 领航专业之路”研学活动圆满结束!

长期美债遭遇砸盘!非农公布前多头不敢贸然入场

南京:对购买新建商品住房实施补贴 对集体土地房屋征收推行房票安置

财政部、水利部紧急下达4.5亿元中央财政水利救灾资金 支持京津冀防汛救灾工作

降准要往后延?央行态度由“综合运用”变为“综合评估”,“最快9月落地”

双位数空置率,全球写字楼租金最贵城市,如今正陷入低潮

下周又要洗澡澡啦!

甲状腺结节食物疗法(甲状腺结节吃什么食物可以消除)

悲哀,谁来救救她 ......

赵丽颖4岁儿子露面,活泼好动抗拒母亲,细节曝与冯绍峰婚姻现状

中道·鹊桥会

青春作伴 圆梦蓉城

延庆、昌平、门头沟、房山区部分公交线路恢复运营

【百万庄小课堂】成都大运会的这些“冷知识”你知道吗?

热度飙升!厦门今日最高温直逼40℃

他们在成都“入乡随俗”!

网传驾校女学员被教练猥亵!官方通报:涉事教练被开除

C视频·大运村的“young”气生活丨第六天,他拿到了一个迷你版的“自己”

天府融媒看大运丨成都大运村里“非遗热” 在这里看见古老又青春的中国

《封神》幕后纪录片《主角的诞生》曝光,揭秘电影十年创作之路

北向资金今日净买入27.42亿元 宁德时代获净买入15.49亿元

创新“双高”行动开展路径|广州“探寻专业之路 启迪学业发展”研学活动圆满结束

济南市第二人民医院巾帼文明岗走进经纬嘉园社区开展老年健康宣传周活动

“油轮战”硝烟再起?美国考虑在商船上部署武装人员!

“奶业全产业链展”在呼和浩特国际农业博览园开展!记者带你来探馆→

房屋财产被淹,中图网遭“毁灭打击”!保险怎么止损?

6年涨价6倍,共享充电宝的钱究竟被谁赚走了:商户占七成,代理称“太卷”

外汇局:上半年来华直接投资保持资金净流入

700亿芯片龙头股闪崩!兆易创新净利预减77%,“行业周期下行压力非常大”

河南灾后重建近百亿资金出问题,谁动了老百姓的救命钱?

上海昆山锦溪古镇宛如桃源居住地

齐心协力 “托”起多彩童年

青海开展农民工工资争议速裁庭建设专项行动

天津路面积水 有市民骑马出行 在街头蹚水遛弯

无人机群出动!俄罗斯对乌克兰展开报复

漫评美国炒作“中国经济胁迫”:“贴标签”

中央民族乐团亮相成都!两场重磅音乐会带你“回味”经典国乐

海外网评:成都大运会不仅有“颜值”,更有“智慧”

中国海油紧急调派直升机驰援涿州,已成功营救49人