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

这篇具有很好参考价值的文章主要介绍了从一道面试题来谈谈Golang中的 ==。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

1、面试题

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

type T interface {

}

func main()  {

	var (
		t T
		p *T

		i1 interface{} = t
		i2 interface{} = p
	)

	fmt.Println(i1 ==t, i1 == nil)
	fmt.Println(i2 ==p, i2 == nil)
	fmt.Println(t == nil)
	fmt.Println(p == nil)

}

执行结果:

true true
true false
true
true

分析:

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

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

2、Golang中的数据类型

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

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

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

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

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

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

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

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

type byte = uint8
type rune = int32

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

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

3、四大类型如何使用 ==

3.1、基本类型

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

package main

import "fmt"

func main() {
	var a int64
	var b int64
	var c int32

	fmt.Println(a == b)
	fmt.Println(c)

	// Invalid operation: a == c (mismatched types int64 and int32)
	//fmt.Println(a == c)
}

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

package main

import "fmt"

func main() {
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(a+b == c)  // false

}

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

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

数字编码

注意:

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

package main

import (
	"fmt"
	"math"
)

func main() {
	var a = 0.1
	var b = 0.2
	var c = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(math.Abs((a+b)-c) < 1e-9) // true
	fmt.Printf("%T", a) // float64
}

3.2、复合类型

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

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

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

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

例如:

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

type S1 struct {
	Name   string
	Scores []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) // true
	fmt.Println(arrayB == arrayC) // false
	fmt.Println("-------")

	s1 := Student{"xiaoming", 18, false}
	s2 := Student{"xiaoming", 18, false}
	s3 := Student{"xiaowang", 18, false}
	fmt.Println(s1 == s2) // true
	fmt.Println(s1 == s3) // false

	fmt.Println("-------")
	a1 := [...]Student{s1, s2}
	// 注意这两个元素!
	a2 := [2]Student{s2, s2}
	a3 := [2]Student{s2, s3}
	fmt.Println(a1 == a2) // true
	fmt.Println(a3 == a2) // false
	fmt.Println("-------")

	var i1 ITest = 23
	var i2 ITest = 23
	var i3 ITest = "tt"
	var i4 ITest = 23

	fmt.Println(i1 == i2) // true
	fmt.Println(i3 == i4) // false
	is1 := [...]ITest{i1, i2}
	is2 := [...]ITest{i1, i4}
	is3 := [...]ITest{i1, i3}

	fmt.Println(is1 == is2) // true
	fmt.Println(is1 == is3) // false

	fmt.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)) // true
	fmt.Println(reflect.DeepEqual(t1, t3)) // false
}

3.3、引用类型

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

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

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

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

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

3.3.1、指针
package main

import (
	"fmt"
)

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	s1 := &Student{"xiaoming", 18, false}
	s2 := &Student{"xiaoming", 18, false}
	s3 := s1
	fmt.Println(s1 == s2) // false
	fmt.Println(s1 == s3) // true
}

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

3.3.2、channel 和 函数类型

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

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	ch1 := make(chan bool, 1)
	ch2 := make(chan bool, 1)
	ch3 := ch1
	fmt.Println(ch1 == ch2) // false
	fmt.Println(ch1 == ch3) // true
	fmt.Println("-----")

	a := TestFunc
	b := TestFunc
	c := 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)  // 0x10a3400
	fmt.Println(b)  // 0x10a3400
	fmt.Println(c)  // 0x10a3400
}

func TestFunc() {

}

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

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

3.3.3、slice

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

a := []interface{}{ 1, 2.0 }
a[1] = a
fmt.Println(a)

// !!!
// runtime: goroutine stack exceeds 1000000000-byte limit
// fatal error: stack overflow

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

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

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

例如:

var a []int
var b []int

// invalid operation: a == b (slice can only be compared to nil)
fmt.Println(a == b)

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

package main

import (
	"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))   // 输出: true

	fmt.Println(slicesAreEqual(slice1, slice3)) // 输出: false
}

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

3.3.4、map

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

当你将一个 map 赋值给另一个变量时,它们引用同一个底层的 map 数据。因此,两个 map 可能引用相同的底层数据,但它们仍然是不同的 map 对象。直接比较两个 map 是否相等,并不能确定它们是否引用同一个底层数据。

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

package main

import (
	"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}
  
  // invalid operation: map1 == map2 (map can only be compared to nil)
	//fmt.Println(map1 == map2)

	fmt.Println(reflect.DeepEqual(map1, map2)) // 输出: true (reflect.DeepEqual 可以进行值相等判断)
	fmt.Println(mapsAreEqual(map1, map2))     // 输出: true

	fmt.Println(mapsAreEqual(map1, map3)) // 输出: false
}

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

注意:

由于map的底层原理是使用到了 hash 表,所以所有不可比较的类型都不能作为mapkey。例如:

// invalid map key type []int
m1 := make(map[[]int]int)

type A struct {
    a []int
    b string
}
// invalid map key type A
m2 := make(map[A]int)

由于切片类型不可比较,不能作为mapkey,编译时m1 := make(map[[]int]int)报错。 由于结构体A含有切片字段,不可比较,不能作为mapkey,编译报错。

3.4、接口类型

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

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

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

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

package main

import "fmt"

func main() {
	var a interface{} = 1
	var b interface{} = 2
	var c interface{} = 1
	var d interface{} = 1.0
	fmt.Println(a == b) // false
	fmt.Println(a == c) // true
	fmt.Println(a == d) // false
}

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

package main

import "fmt"

func main() {
	type A struct {
		a int
		b 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) // true
	fmt.Println(aa == cc) // false

	var 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 []int
fmt.Println(a == b)

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

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

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	var f *os.File

	var r io.Reader = f
	var rc io.ReadCloser = f
	fmt.Println(r == rc) // true

	var w io.Writer = f
	// invalid operation: r == w (mismatched types io.Reader and io.Writer)
	fmt.Println(r == w)
}

type ReadCloser interface {
	Reader
	Closer
}

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 main

import "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之==文章来源地址https://www.toymoban.com/news/detail-625973.html

到了这里,关于从一道面试题来谈谈Golang中的 ==的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java面试题:请谈谈Java中的volatile关键字?

    在Java中,volatile是一种特殊的修饰符,用于确保多线程环境下的变量 可见性和顺序性 。当一个变量被声明为volatile时,它可以确保以下两点: 内存可见性 :当一个线程修改了一个volatile变量的值,其他线程会立即看到这个改变。这是因为volatile会禁止CPU缓存和编

    2024年04月23日
    浏览(54)
  • Python常见面试题016. 请实现如下功能|谈谈你对闭包的理解

    摘自流畅的python 第七章 函数装饰器和闭包 实现一个函数(可以不是函数)avg,计算不断增加的系列值的平均值,效果如下 跟Python常见面试题015.请实现一个如下功能的函数有点类似,但又不太一样 关键是你需要有个变量来存储历史值 参考代码 avg是个Average的实例 avg有个属性

    2023年04月10日
    浏览(39)
  • 2023最新网络安全面试题大全,看完这篇你的秋招offer就到手了!

    随着国家政策的扶持,网络安全行业也越来越为大众所熟知,想要进入到网络安全行业的人也越来越多。 为了拿到心仪的 Offer 之外,除了学好网络安全知识以外,还要应对好企业的面试。 作为一个安全老鸟,工作这么多年,面试过很多人也出过很多面试题目,也在网上收集

    2024年02月07日
    浏览(53)
  • 每日一道面试题之什么是反射?

    反射是一种自我观察的能力,在程序运行时,对任意一个类,我们可通过 class、constructor、field、method 四个方法获取该类的各个组成部分,在java程序运行时,对任意类,我们都可通过该类了解到其包含哪些属性和方法,这种 动态获取当前类对象的信息以及动态调用对象方法的

    2024年02月08日
    浏览(48)
  • 每日一道面试题之介绍一下Iterator

    Iterator是Java中的一个接口 , 用于遍历集合(Collection)中的元素 。通过Iterator,可以 按顺序访问集合中的每个元素 ,而无需了解集合的内部实现细节。 通过调用集合的 iterator()方法获取Iterator对象 。例如: 使用 while循环和hasNext()方法判断是否还有下一个元素 。例如: 使用

    2024年02月15日
    浏览(33)
  • 每天一道面试题:Spring的Bean生命周期

    Spring的Bean生命周期包括以下步骤: 1、 实例化(Instantiation) :当Spring容器接收到创建Bean的请求时,它会先实例化Bean对象。这个过程可以通过构造函数、工厂方法或者反序列化等方式完成; 2、 属性赋值(Populate Properties) :在实例化Bean对象后,Spring容器会通过setter方法或

    2024年02月08日
    浏览(52)
  • 【面试题】谈谈MySQL的索引

    可以把Mysql的索引看做是一本书的目录,当你需要快速查找某个章节在哪的时候,就可以利用目录,快速的得到某个章节的具体的页码。Mysql的索引就是为了提高查询的速度,但是降低了增删改的操作效率,也提高了空间的开销。比如一本书很薄的时候,章节不多,对应的目录

    2024年02月22日
    浏览(35)
  • 每天一道面试题之==和equals的区别是什么?

    \\\"==\\\"是一个关系运算符 ,关系运算符可以用来进行数据和数据之间的比较,而在java中数据类型大致可以分为两大类分别是 基本数据类型 和 引用数据类型 。 基本数据类型 包含 byte,int,float,double,char,boolean,long,short八种类型。 引用类型 包含类,数组,接口三种类型。

    2024年02月06日
    浏览(40)
  • 经典面试题:谈谈对死锁的理解

    死锁是指在并发系统中,两个或多个进程(或线程)因为彼此互相等待对方释放资源而无法继续执行的状态。 简单来说 ,当多个进程都在等待其他进程所持有的资源时,就可能发生死锁。 当一个线程一把锁,连续加锁两次的时候,如果锁是不可重入锁,就会死锁 补充:C+

    2024年02月12日
    浏览(36)
  • 一道面试题:计算时间偏移量,怎么设计你的程序?

    计算时间偏移量,例如,计算当前时间向前偏移 30 秒的时间,我们利用java.util.Calendar很容易实现。   我在组织公司的技术招聘面试的时候,关于程序设计,有问过应聘者这样的问题。 那么,我们怎么封装这么一个工具类呢?这个工具类提供哪些工具方法呢?每个方法又当怎

    2024年02月16日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包