频道栏目
首页 > 程序开发 > 软件开发 > 其他 > 正文
Go语言学习笔记5
2016-07-05 09:17:43      个评论    来源:Huazie的博客  
收藏   我要投稿

3.Go语言数据类型

本篇接着 Go语言学习笔记4 讲Go语言数据类型,主要如下:

3.7 结构体

结构体类型既可以包含若干个命名元素(又称字段),又可以与若干个方法相关联。

1.类型表示法

结构体类型的声明可以包含若干个字段的声明。字段声明左边的标识符表示了该字段的名称,右边的标识符代表了该字段的类型,这两个标识符之间用空格分隔。

结构体类型声明中的每个字段声明都独占一行。同一个结构体类型声明中的字段不能出现重名的情况。

结构体类型也分为命名结构体类型匿名结构体类型

命名结构体类型

命名结构体类型以关键字type开始,依次包含结构体类型的名称、关键字struct和由花括号括起来的字段声明列表。如下:

type Sequence struct {
    len int
    cap int
    Sortable
    sortableArray  sort.Interface
}

结构体类型的字段的类型可以是任何数据类型。当字段名称的首字母是大写字母时,我们就可以在任何位置(包括其他代码包)上通过其所属的结构体类型的值(以下简称结构体值)和选择表达式访问到它们。否则当字段名称的首字母是小写,这些字段就是包级私有的(只有在该结构体声明所属的代码包中才能对它们进行访问或者给它们赋值)。

如果一个字段声明中只有类型而没有指定名称,这个字段就叫做匿名字段。如上结构体 Sequence 中的 Sortable 就是一个匿名字段。匿名字段有时也被称为嵌入式的字段或结构体类型的嵌入类型。

匿名字段的类型必须由一个数据类型的名称或者一个与非接口类型对应的指针类型的名称代表。代表匿名字段类型的非限定名称将被隐含地作为该字段的名称。如果匿名字段是一个指针类型的话,那么这个指针类型所指的数据类型的非限定名称(由非限定标识符代表的名称)就会被作为该字段的名称。非限定标识符就是不包含代码包名称和点的标识符。

匿名类型的隐含名称的实例,如下:

type Anonymities struct {
    T1
    *T2
    P.T3
    *P.T4
}

这个名为 Anonymities 的结构体类型包含了4个匿名字段。其中,T1P.T3 为非指针的数据类型,它们隐含的名称分别为 T1T3*T2*P.T4 为指针类型,它们隐含的名称分别为 T2T4

注意:匿名字段的隐含名称也不能与它所属的结构体类型中的其他字段名称重复。

结构体类型中的嵌入字段的类型所附带的方法都会成为该结构体类型的方法,结构体类型自动实现了它包含的所有嵌入类型所实现的接口类型。但是嵌入类型的方法的接收者类型仍然是该嵌入类型,而不是被嵌入的结构体类型。当在结构体类型中调用实际上属于嵌入类型的方法的时候,这一调用会被自动转发到这个嵌入类型的值上。

现在对 Sequence 的声明进行改动,如下:

type Sequence struct {
    Sortable
    sorted bool
}

上面的 Sequence 中的匿名字段 Sortable 用来存储和操作可排序序列,布尔类型的字段 sorted 用来表示类型值是否已经被排序。

假设有一个 Sequence 类型的值 seq,调用 Sortable 接口类型中的方法 Sort,如下:

seq.Sort()

如果 Sequence 类型中也包含了一个与 Sortable 接口类型中的方法 Sort 的名称和签名相同的方法,那么上面的调用一定是对 Sequence 类型值自身附带的 Sort 方法的调用,而嵌入类型 Sortable 的方法 Sort 被隐藏了。

如果需要在原有的排序操作上添加一些额外功能,可以这样声明一个同名的方法:

func (self *Sequence) Sort() {
    self.Sortable.Sort()
    self.sorted = true
}

这样声明的方法实现了对于匿名字段 SortableSort 方法的功能进行无缝扩展的目的。

如果两个 Sort 方法的名称相同但签名不同,那么嵌入类型 Sortable 的方法 Sort 也同样会被隐藏。这时,在 Sequence 的类型值上调用 Sort 方法的时候,必须依据该 Sequence 结构体类型的 Sort 方法的签名来编写调用表达式。如下声明 Sequence 类型附带的名为 Sort 的方法:

func (self *Sequence) Sort(quicksort bool) {
    //省略若干语句
}

但是调用表达式 seq.Sort() 就会造成一个编译错误,因为 Sortable 的无参数的 Sort 方法已经被隐藏了,只能通过 seq.Sort(true)seq.Sort(false) 来对 SequenceSort 方法进行调用。

注意:无论被嵌入类型是否包含了同名的方法,调用表达式 seq.Sortable.Sort() 总是可以来调用嵌入类 SortableSort 方法。

现在,区别一下嵌入类型是一个非指针的数据类型还是一个指针类型,假设有结构体类型 S 和非指针类型的数据类型 T,那么 *S 表示指向 S 的指针类型,*T 表示指向 T 的指针类型,则:

如果在 S 中包含了一个嵌入类型 T,那么 S*S 的方法集合中都会包含接收者类型为 T 的方法。除此之外,*S 的方法集合中还会包含接收者类型为 *T 的方法。

如果在 S 中包含了一个嵌入类型 *T,那么 S*S 的方法集合中都会包含接收者类型为 T*T 的所有方法。

现在再讨论另一个问题。假设,我们有一个名为 List 的结构体类型,并且在它的声明中嵌入了类型 Sequence,如下:

type List struct {
    Sequence
}

假设有一个 List 类型的值 list,调用嵌入的 Sequence 类型值的字段 sorted,如下:

list.sorted

如果 List 类型也有一个名称为 sorted 的字段的话,那么其中的 Sequence 类型值的字段 sorted 就会被隐藏。

注意: 选择表达式 list.sorted 只代表了对 List 类型的 sorted 字段的访问,不论这两个名称为 sorted 的字段的类型是否相同。和上面的类似,这里选择表达式 list.Sequence.sorted 总是可以访问到嵌入类型 Sequence 的值的 sorted 字段。

对于结构体类型的多层嵌入的规则,有两点需要说明:

可以在被嵌入的结构体类型的值上像调用它自己的字段或方法那样调用任意深度的嵌入类型值的字段或方法。唯一的前提条件就是这些嵌入类型的字段或方法没有被隐藏。如果它们被隐藏,也可以通过类似 list. Sequence.sorted 这样的表达式进行访问或调用它们。

被嵌入的结构体类型的字段或方法可以隐藏任意深度的嵌入类型的同名字段或方法。任何较浅层次的嵌入类型的字段或方法都会隐藏较深层次的嵌入类型包含的同名的字段或方法。注意,这种隐藏是可以交叉进行的,即字段可以隐藏方法,方法也可以隐藏字段,只要它们的名称相同即可。

如果在同一嵌入层次中的两个嵌入类型拥有同名的字段或方法,那么涉及它们的选择表达式或调用表达式会因为编译器不能确定被选择或调用的目标而造成一个编译错误。

匿名结构体类型

匿名结构体类型比命名结构体类型少了关键字type类型名称,声明如下:

struct {
    Sortable
    sorted bool
}

可以在数组类型、切片类型或字典类型的声明中,将一个匿名的结构体类型作为他们的元素的类型。还可以将匿名结构体类型作为一个变量的类型,例如:

var anonym struct {
    a int
    b string
}

不过对于上面,更常用的做法就是在声明以匿名结构体类型为类型的变量的同时对其初始化,例如:

anonym := struct {
    a int
    b string
}{0, "string"}

与命名结构体类型相比,匿名结构体类型更像是“一次性”的类型,它不具有通用性,常常被用在临时数据存储和传递的场景中。

在Go语言中,可以在结构体类型声明中的字段声明的后面添加一个字符串字面量标签,以作为对应字段的附加属性。例如:

type Person struct {
    Name    string `json:"name"`
    Age     uint8 `json:"age"`
    Address string `json:"addr"`
}

如上的字段的字符串字面量标签一般有两个反引号包裹的任意字符串组成。并且,它应该被添加但在与其对应的字段的同一行的最右侧。

这种标签对于使用该结构体类型及其值的代码来说是不可见的。但是,可以用标准库代码包 reflect 中提供的函数查看到结构体类型中字段的标签。这种标签常常会在一些特殊应用场景下使用,比如,标准库代码包 encoding/json 中的函数会根据这种标签的内容确定与该结构体类型中的字段对应的 JSON 节点的名称。

2.值表示法

结构体值一般由复合字面量(类型字面量和花括号构成)来表达。在Go语言中,常常将用于表示结构体值的复合字面量简称为结构体字面量。在同一个结构体字面量中,一个字段名称只能出现一次。例如:

Sequence{Sortable: SortableStrings{"3", "2", "1"}, sorted: false}

类型 SortableStrings 实现了接口类型 Sortable,这个可以在Go语言学习笔记4中了解到。这里就可以把一个 SortableStrings 类型的值赋给 Sortable 字段。

编写结构体字面量,还可以忽略字段的名称,但有如下的两个限制:

如果想要省略其中某个或某些键值对的键,那么其他的键值对的键也必须省略。

Sequence{ SortableStrings{"3", "2", "1"}, sorted: false}//这是不合法的

多个字段值之间的顺序应该与结构体类型声明中的字段声明的顺序一致,并且不能够省略掉任何一字段的赋值。但是不省略字段名称的字面量却没有此限制。例如:

Sequence{ sorted: false , Sortable: SortableStrings{"3", "2", "1"}}//合法
Sequence{SortableStrings{"3", "2", "1"}, false}//合法 
Sequence{ Sortable: SortableStrings{"3", "2", "1"}}//合法,未被明确赋值的字段的值会被其类型的零值填充。
Sequence{ false , SortableStrings{"3", "2", "1"}}//不合法,顺序不一致,会编译错误
Sequence{ SortableStrings{"3", "2", "1"}}//不合法,顺序不一致,会编译错误

在Go语言中,可以在结构体字面量中不指定任何字段的值。例如:

Sequence{}//这种情况下,两个字段都被赋予它们所属类型的零值。

与数组类型相同,结构体类型属于值类型。结构体类型的零值就是如上的不为任何字段赋值的结构体字面量。

3.属性和基本操作

一个结构体类型的属性就是它所包含的字段和与它关联的方法。在访问权限允许的情况下,我们可以使用选择表达式访问结构体值中的字段,也可以使用调用表达式调用结构体值关联的方法。

在Go语言中,只存在嵌入而不存在继承的概念。不能把前面声明的 List 类型的值赋给一个 Sequence 类型的变量,这样的赋值语句会造成一个编译错误。在一个结构体类型的别名类型的值上,既不能调用那个结构体类型的方法,也不能调用与那个结构体类型对应的指针类型的方法。别名类型不是它源类型的子类型,但别名类型内部的结构会与它的源类型一致。

对于一个结构体类型的别名类型来说,它拥有源类型的全部字段,但这个别名类型并没有继承与它的源类型关联的任何方法。

如果只是将 List 类型作为 Sequence 类型的一个别名类型,那么声明如下:

type List Sequence

此时,List 类型的值的表示方法与 Sequence 类型的值的表示方法一样,如下:

List{ SortableStrings{"4", "5", "6"}, false}

如果有一个 List 类型的值 List,那么选择表达式 list.sorted 访问的就是这个 List 类型的值的 sorted 字段,同样,我们也可以通过选择表达式 list.Sortable 访问这个值的嵌入字段 Sortable。但是这个 List 类型目前却不包含与它的源类型 Sequence 关联的方法。

在Go语言中,虽然很多预定义类型都属于泛型类型(比如数组类型、切片类型、字典类型和通道类型),但却不支持自定义的泛型类型。为了使 Sequence 类型能够部分模拟泛型类型的行为特征,只是向它嵌入 Sortable 接口类型是不够的,需要对 Sortable 接口类型进行拓展。如下:

type GenericSeq interface {
    Sortable
    Append(e interface{}) bool
    Set(index int, e interface{}) bool
    Delete(index int) (interface{}, bool)
    ElemValue(index int) interface{}
    ElemType() reflect.Type
    value() interface{}
}

如上的接口类型 GenericSeq 中声明了用于添加、修改、删除、查询元素,以及获取元素类型的方法。一个数据类型要实现 GenericSeq 接口类型,也必须实现 Sortable 接口类型。

现在,将嵌入到 Sequence 类型的 Sortable 接口类型改为 GenericSeq 接口类型,声明如下:

type Sequence struct {
    GenericSeq
    sorted bool
    elemType reflect.Type
}

在如上的类型声明中,添加了一个 reflect.Type 类型(即标准库代码包 reflect 中的 Type 类型)的字段 elemType,目的用它来缓存 GenericSeq 字段中存储的值的元素类型。

为了能够在改变 GenericSeq 字段存储的值的过程中及时对字段 sortedelemType 的值进行修改,如下还创建了几个与 Sequence 类型关联的方法。声明如下:

func (self *Sequence) Sort() {
    self.GenericSeq.Sort()
    self.sorted = true
}

func (self *Sequence) Append(e interface{}) bool{
    result := self. GenericSeq.Append(e)
    //省略部分代码
    self.sorted = true
    //省略部分代码
    return result
}

func (self *Sequence) Set(index int, e interface{}) bool {
    result := self. GenericSeq.Set(index, e)
    //省略部分代码
    self.sorted = true
    //省略部分代码
    return result
}

func (self *Sequence) ElemType() reflect.Type {
    //省略部分代码
    self.elemType = self.GenericSeq.ElemType()
    //省略部分代码
    return self.elemType
}

如上的这些方法分别与接口类型 GenericSeqSortable 中声明的某个方法有着相同的方法名称和方法签名。通过这种方式隐藏了 GenericSeq 字段中存储的值的这些同名方法,并对它们进行了无缝扩展。

3.8指针

指针是一个代表着某个内存地址的值。这个内存地址往往是在内存中存储的另一个变量的值的起始位置。Go语言既没有像Java语言那样取消了代码对指针的直接操作的能力,也避免了C/C++语言中由于对指针的滥用而造成的安全和可靠性问题。

Go语言的指针类型指代了指向一个给定类型的变量的指针。它常常被称为指针的基本类型。指针类型是Go语言的复合类型之一。

1.类型表示法

可以通过在任何一个有效的数据类型的左边加入 * 来得到与之对应的指针类型。例如,一个元素类型为 int 的切片类型所对应的指针类型是 *[]int ,前面的结构体类型 Sequence 所对应的指针类型是 *Sequence

注意:如果代表类型的是一个限定标识符(如 sort.StringSlice),那么表示与其对应的指针类型的字面量应该是 *sort.StringSlice ,而不是 sort.*StringSlice

在Go语言中,还有一个专门用于存储内存地址的类型 uintptr。而 uintptr 类型与 int 类型和 uint 类型一样,也属于数组类型。它的值是一个能够保存一个指针类型值(简称指针值)的位模式形式。

2.值表示法

如果一个变量 v 的值是可寻址的,表达式 &v 就代表了指向变量 v 的值的指针值。

知识点: 如果某个值确实被存储在了计算机中,并且有一个内存地址可以代表这个值在内存中存储的起始位置,那么就可以说这个值以及代表它的变量是可寻址的

3.属性和基本操作

指针类型属于引用类型,它的零值是 nil

对指针的操作,从标准代码包 unsafe 讲起,如下为省略文档的 unsafe 包下面的 unsafe.go源码(亲们可以自己到Go安装包 src 目录查看详细内容):

package unsafe

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

在代码包 unsafe 中,有一个名为 ArbitraryType 的类型。从类型声明上看,它是 int 类型的一个别名类型。但是,它实际上可以代表任意的Go语言表达式的结果类型。事实上,它也并不算是 unsafe 包的一部分,在这里声明它仅处于代码文档化的目的。另外 unsafe 还声明了一个名为 Pointer 的类型,它代表了ArbitraryType 类型的指针类型。

如下有4个与 unsafe.Pointer 类型相关的特殊转换操作:

一个指向其他类型的指针值都可以被转换为一个unsafe.Pointer类型值。例如,如果有一个float32类型的变量f32,那么可以将与它的对应的指针值转换为一个unsafe.Pointer类型的值:

pointer := unsafe.Pointer(&f32)

其中,在特殊标记 := 右边就是用于进行转换操作的调用表达式。取值表达式 &f32 的求值结果是一个 *float32 类型的值。

一个 unsafe.Pointer 类型值可以被转换为一个与任何类型对应的指针类型的值。例如:

vptr := (*int)(pointer)

上面的代码用于将 pointer 的值转换为与指向int类型值的指针值,并赋值给变量 vptrint 类型和 *float32 类型在内存中的布局是不同的,如果我们在它们之上直接进行类型转换(对应表达式* (int)(&f32))* 是不行,这会产生一个编译错误。有了上面的 unsafe.Pointer 作为中转类型的时候,看起来操作没有问题,但在使用取值表达式 *vptr 的时候会出现问题,int 类型的值和 float32 类型的值解析得到的结果是完全不同的,这样会产生一个不正确的结果。比如,如果这里对变量 vptr 的赋值语句改为:

vptr := (*string)(pointer)

取值表达式 *vptr的求值就会引发一个运行时恐慌。

一个 uintptr 类型的值也可以被转换为一个 unsafe.Pointer 类型的值。例如:

pointer2 := unsafe.Pointer(uptr)

一个 unsafe.Pointer 类型值可以被转换为一个 uintptr 类型的值。例如:

uptr := uintptr(pointer)

注意:正是因为这些特殊的转换操作,unsafe.Pointer 类型可以使程序绕过Go语言的类型系统并在任意的内存地址上进行读写操作成为可能。但这些操作非常危险,小心使用

现在用之前的结构体类型 Person 举例,如下:

type Person struct {
    Name    string `json:"name"`
    Age     uint8 `json:"age"`
    Address string `json:"addr"`
}

初始化 Person 的值,并把它的指针值赋给变量 p :

p := &Person(“Huazie”, 23, “Nanjing”)

下面利用上述特殊转换操作中的第一条和第三条获取这个结构体值在内存中的存储地址:

var puptr = uintptr(unsafe.Pointer(p))

变量 puptr 的值就是存储上面那个 Person 类型值的内存地址。由于类型 uintptr 的值实际上是一个无符号整数,所以我们可以在该类型的值上进行任何算术运算。例如:

var np uintptr = puptr + unsafe.Offsetof(p.Name) //变量np表示结构体中的Name字段值的内存地址。

如上 unsafe.Offsetof 函数会返回作为参数的某字段(由相应的选择表达式表示)在其所属的结构体类型之中的存储偏移量。也就是,在内存中从存储这个结构体值的起始位置到存储其中某字段的值的起始位置之间的距离。这个存储偏移量(或者说距离)的单位是字节,它的值的类型是 uintptr。对于同一个结构体类型和它的同一个字段来说,这个存储偏移量总是相同的。

在获得存储 Name 字段值的内存地址之后,将它还原成指向这个 Name 字段值的指针类型值,如下:

var name *string = (*string)(unsafe.Pointer(np))

获取这个 Name 字段的值:

*name

只要获得了存储某个值的内存地址,就可以通过一定的算术运算得到存储在其他内存地址上的值甚至程序。如下一个恒等式显示上面的一些操作:

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

3.9数据初始化

这里的数据的初始化是指对某个数据类型的值或变量的初始化。在Go语言中,几乎所有的数据类型的值都可以使用字面量来进行表示和初始化。在大多数情况下,使用字面量就可以满足初始化值或变量的要求。

Go语言还提供了两个专门用于数据初始化的内建函数 newmake

1.new

内建函数 new 用于为值分配内存。它并不会去初始化分配到的内存,而只会清零它。例如:

new(T)

如上调用该表达式求值时,所做的是为 T 类型的新值分配并清零一块内存空间,然后将这块内存空间的地址作为结果返回。而这个结果就是指向这个新的 T 类型值的指针值。它的类型为 *Tnew 函数返回的 *T 类型值总是指向一个 T 类型的零值。例如:

new(string)//求值结果指向的是一个string类型的零值""
new([3]int)//求值结果指向的是一个[3]int类型的零值[3]int{0,0,0}

以标准库代码包 bytes 中的结构体类型 Buffer 为例, bytes.Buffer 是一个尺寸可变的字节缓冲区。它的零值就是一个立即可用的空缓冲区。例如:

new(bytes.Buffer)//求值结果就是一个指向一个空缓冲区的指针值,可以立即在这个缓冲区上进行读写操作

标准库代码包 sync 中的结构体类型 Mutex 也是一个可以 new 后即用的数据类型。它的零值就是一个处于未锁定状态的互斥量。

2.make

内建函数 make 只能被用于创建切片函数、字典类型和通道类型的值,并返回一个已被初始化的(即非零值的)的对应类型的值。以上3个复合类型的特殊结构都是引用类型,在它们的每一个值的内部都会保持着一个对某个底层数据结构值的引用。如果不对它们的值进行初始化,那么其中的这种引用关系是不会建立起来的,同时相关的内部值也会不正确。因此在创建这3个引用类型的值的时候,必须将内存空间分配和数据初始化这两个步骤绑定在一起。

内建函数 make 除了会接受一个表示目标类型的类型字面量之外,还会接受一个或两个额外的参数。

对于切片类型来说,可以把新值的长度和容量也传递给 make 函数,例如:

make([]int, 10, 100)

如上创建了一个新的 []int 类型的值,这个值的长度为 10,容量为 100。如果省略最后一个参数,即不指定新值的容量。这样的话,该值的容量会与其长度一致。例如:

s := make{[]int, 10}

变量 s 的类型是 []int 的,而长度和容量都是 10

在使用 make 函数初始化一个切片值的过程中,该值会引用一个长度与其容量相同且元素类型与其元素类型一致的数组值。这个数组值就是该切片值的底层数组。该数组值中每个元素都是当前元素类型的零值。但是,切片值只会展现出数量与其长度相同的元素。因此 make([]int, 10, 100) 所创建并初始化的值就是 []int{0,0,0,0,0,0,0,0}

在使用 make 函数创建字典类型的值的时候,也可以指定其底层数据结果的长度。但是,该字典值只会展示出我们明确“放入”的键值对。例如:

make(map[string]int, 100)

它所创建和初始化的值会使 map[string]int{}。虽然可以忽略那个用于表示底层数据结构长度的参数,但是这边还是建议:应该在性能敏感的应用场景下,根据这个字典值可能包含的键值对的数量以及“放入”它们的时间,仔细地设置该长度参数。

对于通道类型的值的数据初始化,这里可以使用 make 函数创建一个通道类型的值:

make(chan int, 10)

其中的第一个参数表示的是通道的类型,而第二个参数则表示该通道的长度。与字典类型相同,第二个参数也可以被忽略掉。对于忽略它的含义,在之后的博文中详细讲解通道类型的时候大家可以了解。

内建函数 make 只能被应用在引用类型的值的创建上。并且,它的结果是第一个参数所代表的类型的值,而不是指向这个值的指针值。如果想要获取该指针值的话,只需要如下:

m := make(map[string]int , 100)
mp := &m

对于数据初始化需要考虑的一些规则:

字面量可以被用于初始化几乎所有的Go语言数据类型的值,除了接口类型和通道类型。接口类型没有值,而通道类型的值只能使用 make 函数来创建。如果需要指向值的指针值,可以在表示该值的字面量之上进行取址操作。

内建函数 new 主要用于创建值类型的值。调用 new 函数的额表达式的结果值将会是指向被创建值的指针值,并且被创建值会是其所属数据类型的零值。因此,new 函数不适合被用来创建引用类型的值。其直接的原因是引用类型的值的零值都是 nil,是不可用的。

内建函数 make 仅被用于某些引用类型(切片类型、字典类型和通道类型)的指的创建。它在创建值之后还会对其进行必要的初始化。与 new 函数不同,调用 make 函数的表达式的结果值将会是被创建的值本身,而不是指向它的指针值。

Go数据类型的知识就记到这,下一篇讲解数据的使用。关于通道类型,比较特殊,将会在后续的博文仔细讲解,敬请期待…

最后附上知名的Go语言开源框架(每篇更新一个):

Skynet: 一个分布式服务框架。它可以帮助我们构建起大规模的分布式应用系统。它的源码放置在https://github.com/skynetservices/skynet上。

点击复制链接 与好友分享!回本站首页
相关TAG标签 语言学习 笔记
上一篇:HDU2028 Lowest Common Multiple Plus
下一篇:spring注解的使用
相关文章
图文推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站