Go 学习笔记(33)— Go 自定义类型 type(自定义结构体、结构体初始化、结构体内嵌、自定义接口)

1. 自定义类型格式

用户自定义类型使用关键字 type ,其语法格式是:

type newType oldType

oldType 可以是自定义类型、预声明类型、未命名类型中的任意一种。

newType 是新类型的标识符,与 oldType 具有相同的底层类型,并且都继承了底层类型的操作集合(这里的操作不是方法,比如底层类型是 map ,支持 range 迭代访问,则新类型也可以使用 range 迭代访问) 。除此之外, newTypeoldType 是两个完全不同的类型, newType 不会继承 oldType 的方法。

无论 oldType 是什么类型,使用 type 声明的新类型都是一种命名类型,也就是说,自定义类型都是命名类型。

type INT int //INT 是一个使用预声明类型声明的自定义类型
type Map map[string]string //Map 是一个使用类型字面量声明的自定义类型
type myMap Map //myMap 是一个自定义类型Map 声明的自定义类型
// INT, Map 、myMap 都是命名类型

2. 自定义 struct 类型

struct 类型是 Go 语言自定义类型的普遍的形式,是 Go 语言类型扩展的基石,也是 Go 语言面向对象承载的基础。

前面章节将 struct 划为未命名类型,那时的 struct 是使用字面量来表示的,如果使用 type 语句声明,则这个新类型就是命名类型。例如:

// 使用 type 自定义的结构类型属于命名类型
type XXXName struct {
    field1 type1
    field2 type2
}

// errorString 是一个自定义结构类型,也是命名类型
type errorString struct {
    s string
}

// 结构字面量属于未命名类型
struct {
    field1 type1
    field2 type2
}

// struct{} 是非命名类型空结构
var s = struct{}{}

2.1 struct 初始化

Person 结构为例来讲一下结构的初始化的方法。例如:

type Person struct {
    name string
    age int
}
  1. 按照字段顺序进行初始化
// 以下三种写法都可以
a := Person{"Tom", 25}

b := Person{
    	"Tom",
          25}

c := Person{
    "Tom",
      25,	// 必须加逗号
      }

不推荐这种写法,一旦结构体增加字段,则需要重新修改初始化语句的顺序。

  1. 指定字段名进行初始化

推荐这种写法,结构体增加字段后,无需修改原有初始化语句的顺序。

a := Person{name:"Tom", age:25}

b := Person{
    name:"Tom", 
    age:25}

b := Person{
    name:"Tom", 
    age:25, // 加逗号
}	// 初始化语句结尾的 } 独占一行,则最后一个字段的后面一定要带上逗号。
  1. 使用 new 创建内置函数,字段默认初始化为其类型的零值, 返回值是指向结构的指针。例如:
p := new(Person)
// 此时 name 为 "", age 为 0

这种方法不常用,一般使用 struct 都不会将所有字段初始化为零值。

  1. 一次初始化一个字段
p := Person{}
p.name = "Tom"
p.age = 25

这种方法不常用,这是一种结构化的编程思维,没有封装,违背了 struct 本身抽象封装的理念。

  1. 使用构造函数进行初始化

这是推荐的一种方法,当结构发生变化时,构造函数可以屏蔽细节。

type Cat struct {
	Color string
	Name  string
}

func NewCatByName(name string) *Cat {
	return &Cat{
		Name: name, // 忽略 Color 字段
	}
}

func NewCatByColor(color string) *Cat {
	return &Cat{
		Color: color, // 忽略 Name 字段
	}
}
  1. 带有父子关系结构体初始化
type Cat struct {
	Color string
	Name  string
}

type BlackCat struct {
	Cat
}

// 基类
func NewCat(name string) *Cat {
	return &Cat{
		Name: name, // 忽略 Color 字段
	}
}

// 子类
func NewBlackCat(color string) *BlackCat {
	cat := &BlackCat{} // 实例化 BlackCat 结构,此时 Cat 也同时被实例化。

	// 填充 BlackCat 中 嵌入的 Cat 颜色属性。 BlackCat 没有任何成员,所有的成员都来自于 Cat
	cat.Color = color
	return cat
}

2.2 结构字段的特点

结构的字段可以是任意的类型,基本类型、接口类型、指针类型、函数类型都可以作为 struct 的字段。结构字段的类型名必须唯一, struct 字段类型可以是普通类型,也可以是指针。另外,结构支持内嵌自身的指针,这也是实现树形和链表等复杂数据结构的基础。例如:

// 标准库container/list
type Element struct {
// 指向自身类型的指针
    next, prev *Element
    list *List
    Value interface{}
}

2.3 匿名字段

结构体允许其成员字段在声明时没有宇段名而只有类型,这种形式的字段被称为类型内嵌或匿名字段。

在定义 struct 的过程中,如果字段只给出字段类型,没有给出宇段名, 则称这样的字段为“匿名字段”。被匿名嵌入的字段必须是命名类型或命名类型的指针,类型字面量不能作为匿名字段使用。

type Data struct {
	int
	float32
	bool
}

func main() {
	d := &Data{
		int:     10,
		float32: 3.14,
		bool:    false,
	}
	fmt.Printf("%+v", d)
}

类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而己,,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

匿名字段的字段名默认就是类型名,如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。但一个结构体里面不能同时存在某一类型及其指针类型的匿名字段, 原因是二者的字段名相等。如果嵌入的字段来自其他包,则需要加上包名,并且必须是其他包可以导出的类型。

2.4 结构体内嵌

结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。

2.4.1 结构体内嵌示例

// 基础颜色
type BasicColor struct {
	R, G, B float32
}

// 完整颜色
type Color struct {
	// 将基本颜色作为成员
	Basic BasicColor
	// 透明度
	Alpha float32
}

func main() {
	c := new(Color)
	// 设置基本颜色值
	c.Basic.B = 1
	c.Basic.G = 1
	c.Basic.R = 0

	c.Alpha = 0.5
	fmt.Printf("%+v", c)
}

需要通过 Basic 结构才能设置 RGB 分量,虽然合理但是写法很复杂。使用 Go 语言的结构体内嵌写法重新调整代码如下 :

type BasicColor struct {
	R, G, B float32
}

type Color struct {
	// 将 BasicColor 结构体嵌入到 Color 结构体中, 
	// BasicColor 没有宇段名而只有类型,这种写法就叫做结构体内嵌 。
	BasicColor

	Alpha float32
}

func main() {
	c := new(Color)

// 可以直接对 Color 的 R、 G 、 B 成员进行设置,编译器通过 Color 的
// 定义知道 R、 G 、 B 成员来自 BasicColor 内嵌的结构体。
	c.B = 1
	c.G = 1
	c.R = 0

	c.Alpha = 0.5
	fmt.Printf("%+v", c)
}

2.4.2 结构体内嵌特性

  • 内嵌的结构体可以直接访问其成员变量

嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的宇段。
例如, ins.a.b.c 的访问可以简化为 ins.c

  • 内嵌结构体的字段名是它的类型名

内嵌结构体字段仍然可以使用详细的宇段进行一层层访问,内嵌结构体的字段名就是它的类型名,代码如下:

	c := new(Color)

	c.BasicColor.B = 1
	c.BasicColor.G = 1
	c.BasicColor.R = 0

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

3. 自定义接口类型

接口字面量是非命名类型,但自定义接口类型是命名类型。自定义接口类型同样使用 type 关键字声明。示例如下:

// interface{} 是接口字面量类型标识, 所以 i 是非命名类型交量
var i interface{}
// Reader 是自定义接口类型,属于命名类型
type Reader interface {
    Read (p []byte) (n int , err error )
}

4. 为什么要使用类型定义

类型定义可以在原类型的基础上创造出新的类型,有些场合下可以使代码更加简洁,如下边示例代码:

package main

import (
	"fmt"
)

// 定义一个接收一个字符串类型参数的函数类型
type handle func(str string)

// exec函数,接收handle类型的参数
func exec(f handle) {
	f("hello")
}

func main() {
	// 定义一个函数类型变量,这个函数接收一个字符串类型的参数
	var p = func(str string) {
		fmt.Println("first", str)
	}
	exec(p)

	// 匿名函数作为参数直接传递给exec函数
	exec(func(str string) {
		fmt.Println("second", str)
	})
}

输出结果:

first hello
second hello

上边的示例是类型定义的一种简单应用场合,如果不使用类型定义,那么想要实现上边示例中的功能,应该怎么书写这段代码呢?

// exec函数,接收handle类型的参数
func exec(f func(str string)) {
    f("hello")
}

exec 函数中的参数类型,需要替换成 func(str string) 了,咋一看去也不复杂,但是假如 exec 接收一个需要 5 个参数的函数变量呢?是不是感觉参数列表就会很长了。

func exec(f func(str string, str2 string, num int, money float64, flag bool)) {
    f("hello")
}

从上边的代码可以发现,exec 函数的参数列表可读性变差了。下边再来看看使用类型定义是怎么实现这个功能:

package main

import (
    "fmt"
)

// 定义一个需要五个参数的函数类型
type handle func(str string, str2 string, num int, money float64, flag bool)

// exec函数,接收handle类型的参数
func exec(f handle) {
    f("hello", "world", 10, 11.23, true)
}

func demo(str string, str2 string, num int, money float64, flag bool) {
    fmt.Println(str, str2, num, money, flag)
}

func main() {
    exec(demo)
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值