1. 类型别名定义

定义类型别名的写法为:

type TypeAlias = Type

类型别名规定: TypeAlias 只是 Type 的别名,本质上 TypeAliasType 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

2. 类型定义

类型定义语法如下:

type newType Type

其中 newType 是一种新的类型, newType 本身依然具备 Type 类型的特性。新类型与底层类型不能直接相互赋值和运算,如果需要,需要显式转换。

var m int = 5
var n int32 = 6
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。

为了说明类型声明,我们将不同温度单位分别定义为不同的类型:

package tempconvtype Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度const (AbsoluteZeroC Celsius = -273.15 // 绝对零度FreezingC     Celsius = 0       // 结冰点温度BoilingC      Celsius = 100     // 沸水温度
)func CToF(c Celsius) Fahrenheit {return Fahrenheit(c*9/5 + 32)
}func FToC(f Fahrenheit) Celsius {return Celsius((f - 32) * 5 / 9)
}

我们在这个包声明了两种类型: CelsiusFahrenheit 分别对应不同的温度单位。它们虽然有着相同的底层类型 float64 ,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。

刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似 Celsius(t)Fahrenheit(t) 形式的显式转型操作才能将 float64 转为对应的类型。

Celsius(t)Fahrenheit(t) 是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面, CToFFToC 两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

对于每一个类型 T ,都有一个对应的类型转换操作 T(x) ,用于将 x 转为 T 类型(译注:如果 T 是指针类型,可能会需要用小括弧包装 T,比如(*int)(0))。

只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果 x 是可以赋值给 T 类型的值,那么 x 必然也可以被转为 T 类型,但是一般没有这个必要。

数值类型之间的转型也是允许的,并且在字符串和一些特定类型的 slice 之间也是可以转换的。

例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的 slice 将拷贝一个字符串数据的副本。在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。

底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。这意味着, CelsiusFahrenheit 类型的算术运算行为和底层的 float64 类型是一样的,正如我们所期望的那样。

fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC)       // compile error: type mismatch

比较运算符==<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

注意最后那个语句。尽管看起来像函数调用,但是 Celsius(f) 是类型转换操作,它并不会改变值,仅仅是改变值的类型而已。测试为真的原因是因为 cg 都是零值。

一个命名的类型可以提供书写方便,特别是可以避免一遍又一遍地书写复杂类型(译注:例如用匿名的结构体定义变量)。虽然对于像 float64 这种简单的底层类型没有简洁很多,但是如果是复杂的类型将会简洁很多,特别是我们即将讨论的结构体类型。

命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。

下面的声明语句, Celsius 类型的参数 c 出现在了函数名的前面,表示声明的是 Celsius 类型的一个名叫 String 的方法,该方法返回该类型对象 c 带着 °C 温度单位的字符串:

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c)
}

许多类型都会定义一个 String 方法,因为当使用 fmt 包的打印方法时,将会优先使用该类型对应的 String 方法返回的结果打印。

c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c)   // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c)   // "100°C"
fmt.Println(c)          // "100°C"
fmt.Printf("%g\n", c)   // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String

3. 类型别名与类型定义差异

类型别名与类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?下面通过一段代码来理解。

package mainimport ("fmt"
)// 将NewInt定义为int类型
// 通过 type 关键字的定义,NewInt 会形成一种新的类型,NewInt 本身依然具备 int 类型的特性。
type NewInt int// 将int取一个别名叫IntAlias, 将 IntAlias 设置为 int 的一个别名,使 IntAlias 与 int 等效。
type IntAlias = intfunc main() {// 将a声明为NewInt类型var a NewInt// 查看a的类型名fmt.Printf("a type: %T\n", a)  // a type: main.NewInt// 将 b 声明为IntAlias类型var b IntAlias// 查看b的类型名fmt.Printf("b type: %T\n", b)   // b type: int
}

结果显示 a 的类型是 main.NewInt ,表示 main 包下定义的 NewInt 类型,b 类型是 intIntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。

4. 非本地类型不能定义方法

能够随意地为各种类型起名字,是否意味着可以在自己包里为这些类型任意添加方法呢?参见下面的代码演示:

package mainimport ("time"
)// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration// 为 MyDuration 添加一个方法
func (m MyDuration) EasySet(a string) {}func main() {}

错误信息:

./hello.go:11:6: cannot define new methods on non-local type time.Duration

编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法,非本地类型指的就是 time.Duration 不是在 main 包中定义的,而是在 time 包中定义的,与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。

修改方案为将第 8 行类型别名修改为类型定义,如下:

type MyDuration time.Duration

5. 在结构体成员嵌入时使用别名

当类型别名作为结构体嵌入的成员时会发生什么情况呢?请参考下面的代码。

package mainimport ("fmt""reflect"
)// 定义商标结构
type Brand struct {}// 为商标结构添加Show()方法
func (t Brand) Show() {}// 为Brand定义一个别名FakeBrand
type FakeBrand = Brand// 定义车辆结构
type Vehicle struct {// 嵌入两个结构FakeBrandBrand
}func main() {// 声明变量a为车辆类型var a Vehicle// 指定调用FakeBrand的Showa.FakeBrand.Show()// 取a的类型反射对象ta := reflect.TypeOf(a)// 遍历a的所有成员for i := 0; i < ta.NumField(); i++ {// a的成员信息f := ta.Field(i)// 打印成员的字段名和类型fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.Name())}
}

输出结果:

FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand

这个例子中,FakeBrand 是 Brand 的一个别名,在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味着嵌入两个 Brand,FakeBrand 的类型会以名字的方式保留在 Vehicle 的成员中。

如果尝试将第 33 行改为:

a.Show()

编译器将发生报错:

ambiguous selector a.Show

在调用 Show() 方法时,因为两个类型都有 Show() 方法,会发生歧义,证明 FakeBrand 的本质确实是 Brand 类型。

6. 函数也是类型,可以作为参数传递给别的函数

package maintype math func(int, int) int //定义一个函数类型,两个 int 参数,一个 int 返回值//定义一个函数 add,这个函数两个 int 参数一个 int 返回值,与 math 类型相符
func add(i int, j int) int {return i + j
}//再定义一个 multiply,这个函数同样符合 math 类型
func multiply(i, j int) int {return i * j
}//foo 函数,需要一个 math 类型的参数,用 math 类型的函数计算第 2 和第 3 个参数数字,并返回计算结果
//稍后在 main 中我们将 add 函数和 multiply 分别作为参数传递给它
func foo(m math, n1, n2 int) int {return m(1, 2)
}func main() {//传递 add 函数和两个数字,计算相加结果n := foo(add, 1, 2)println(n)//传递 multply 和两个数字,计算相乘结果n = foo(multiply, 1, 2)println(n)
}

7. type 类型用法

type 有如下几种用法:

  • 定义结构体
  • 定义接口
  • 类型定义
  • 类型别名
  • 类型查询

7.1 定义结构体

结构体是用户自定义的一种抽象的数据结构, Golangstruct 类似于 Java 语言中的 class ,在程序设计中,有着举足轻重的地位。结构体的用法,将会在 struct 关键字中详细的介绍。下边来看一下定义一个结构体的语法格式:

type name struct {Field1  dataTypeField2  dataTypeField3  dataType
}

7.2 定义接口

接口相关知识点,将会在 interface 关键字中详细介绍,下边来看一段定义接口的语法格式:

type name interface{Read()Write()
}

7.3 类型定义

使用类型定义定义出来的类型与原类型不相同,所以不能使用新类型变量赋值给原类型变量,除非使用强制类型转换。下面来看一段示例代码,根据 string 类型,定义一种新的类型,新类型名称是 name

type name string

为什么要使用类型定义呢?

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

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)
}

7.4 类型别名

类型别名这个特性在 Golang 1.9 中引入。使用类型别名定义出来的类型与原类型一样,即可以与原类型变量互相赋值,又拥有了原类型的所有方法集。给 strng 类型取一个别名,别名名称是 name

type name = string

类型别名与类型定义不同之处在于,使用类型别名需要在别名和原类型之间加上赋值符号( = );使用类型别名定义的类型与原类型等价,而使用类型定义出来的类型是一种新的类型。

如下边示例:

package main
import ("fmt"
)
type a = string
type b string
func SayA(str a) {fmt.Println(str)
}
func SayB(str b) {fmt.Println(str)
}
func main() {var str = "test"SayA(str)//错误参数传递,str是字符串类型,不能赋值给b类型变量SayB(str)
}

这段代码在编译时会出现如下错误:

.\main.go:21:6: cannot use str (type string) as type b in argument to SayB

从错误信息可知, str 为字符串类型,不能当做 b 类型参数传入 SayB 函数中。而 str 却可以当做 a 类型参数传入到 SayA 函数中。由此可见,使用类型别名定义的类型与原类型一致,而类型定义定义出来的类型,是一种新的类型。

给类型别名新增方法,会添加到原类型方法集中

给类型别名新增方法后,原类型也能使用这个方法。下边请看一段示例代码:

package main
import ("fmt"
)
// 根据string类型,定义类型S
type S string
func (r *S) Hi() {fmt.Println("S hi")
}
// 定义S的类型别名为T
type T = S
func (r *T) Hello() {fmt.Println("T hello")
}
// 函数参数接收S类型的指针变量
func exec(obj *S) {obj.Hello()obj.Hi()
}
func main() {t := new(T)s := new(S)exec(s)// 将T类型指针变量传递给S类型指针变量exec(t)
}

输出信息是:

T hello
S hi
T hello
S hi

上边的示例中,S 是原类型,T 是 S 类型别名。在给 T 增加了 Hello 方法后,S 类型的变量也可以使用 Hello 方法。说明给类型别名新增方法后,原类型也能使用这个方法。从示例中可知,变量 t 可以赋值给 S 类型变量 s,所以类型别名是给原类型取了一个小名,本质上没有发生任何变化。

类型别名,只能对同一个包中的自定义类型产生作用。举个例子,Golang SDK 中有很多个包,是不是我们可以使用类型别名,给 SDK 包中的结构体类型新增方法呢?答案是:不行。请牢记一点:类型别名,只能对包内的类型产生作用,对包外的类型采用类型别名,在编译时将会提示如下信息:

cannot define new methods on non-local type string

7.5 类型查询

类型查询,就是根据变量,查询这个变量的类型。为什么会有这样的需求呢?

Goalng 中有一个特殊的类型 interface{} ,这个类型可以被任何类型的变量赋值,如果想要知道到底是哪个类型的变量赋值给了 interface{} 类型变量,就需要使用类型查询来解决这个需求,示例代码如下:

package main
import ("fmt"
)
func main() {// 定义一个interface{}类型变量,并使用string类型值”abc“初始化var a interface{} = "abc"// 在switch中使用 变量名.(type) 查询变量是由哪个类型数据赋值。switch v := a.(type) {case string:fmt.Println("字符串")case int:fmt.Println("整型")default:fmt.Println("其他类型", v)}
}

如果使用 .(type) 查询类型的变量不是 interface{} 类型,则在编译时会报如下错误:

cannot type switch on non-interface value a (type string)

如果在 switch 以外地方使用 .(type) ,则在编译时会提示如下错误:

use of .(type) outside type switch

所以,使用 type 进行类型查询时,只能在 switch 中使用,且使用类型查询的变量类型必须是 interface{}

Go 学习笔记(27)— type 关键字(类型定义、类型别名、类型查询、定义接口、定义结构体)相关推荐

  1. oracle 表复制 long,【学习笔记】Oracle数据库使用copy实现long类型转移表空间案例 ORA-00997...

    天萃荷净 使用copy实现long类型转移表空间,表空间的数据文件损坏,在转移该表空间相关表时,遇到让人郁闷的long类型.不能使用ctas和move来实现转移,最后通过古老的copy来实现该项工作. ...

  2. 英伟达DeepStream学习笔记27——deepstream下载历史版本

    英伟达DeepStream学习笔记27--deepstream下载历史版本 https://docs.nvidia.com/metropolis/deepstream-archive.html htt ...

  3. c语言结构体定义坐标,C/C++知识点之c语言结构体定义的几种形式

    本文主要向大家介绍了C/C++知识点之c语言结构体定义的几种形式,通过具体的内容向大家展示,希望对大家学习C/C++知识点有所帮助. 1.最常用定义方式:定义结构体data,此时结构体相当于一个类型, ...

  4. C语言学习笔记06-占位符格式、C基本类型及逃逸字符一些细节(附介绍BCD码)

    主要整理有关占位符格式与逃逸字符的一些细节 朋友们,看栗子--"BCD解码" (文末附BCD码介绍) 一个BCD数的十六进制是0x12(对应二进制表示:0001 0010),它表达 ...

  5. python面向对象编程72讲_2020-07-22 Python学习笔记27类和面向对象编程

    一些关于自己学习Python的经历的内容,遇到的问题和思考等,方便以后查询和复习. 声明:本人学习是在扇贝编程通过网络学习的,相关的知识.案例来源于扇贝编程.如果使用请说明来源. 第27关 类与面向对 ...

  6. 影像组学视频学习笔记(27)-SimpleITK包介绍、Li‘s have a solution and plan.

    本笔记来源于B站Up主: 有Li 的影像组学的系列教学视频 本节(27)主要讲解: 功能强大的图像处理工具SimpleITK包 视频中李博士演示了SimpleITK的两个基本功能:图像格式转换以及图像 ...

  7. C++Primer学习笔记:第2章 变量和基本类型

    空类型不对应具体的值,仅用于一些特殊的场合 long的长度为32位,float有7个有效位,double有16个有效位 如果数值超过了int的范围,应该用long long而不是long,long一般 ...

  8. 《机器学习实战》学习笔记:绘制树形图使用决策树预测隐形眼镜类型

    上一节实现了决策树,但只是使用包含树结构信息的嵌套字典来实现,其表示形式较难理解,显然,绘制直观的二叉树图是十分必要的.Python没有提供自带的绘制树工具,需要自己编写函数,结合Matplotlib ...

  9. SQLite学习笔记(八)-- BLOB数据的插入与查询(C++实现)

    1.什么是BLOB数据 BLOB (binary large object)即二进制大对象,是一种可以存储二进制文件的容器.在计算机中,BLOB常常是数据库中用来存储二进制文件的字段类型.常见的BLO ...

  10. C Programming学习笔记【谭浩强老师编】(第四章选择结构程序设计)02 逻辑运算符和逻辑表达式

    文章目录 一.逻辑运算符和逻辑表达式 二.条件运算符和条件表达式 举例2.1 三.选择结构的嵌套 举例3.1 四.用switch语句实现多分支结构 举例4.1 举例4.2 五.选择结构程序综合举例 选 ...

最新文章

  1. 如何在ESXi 5.5主机上安装ESXi 5.5客户机
  2. 使用Eclipse-Maven-git做Java开发(13)--导入git仓库的代码到eclipse
  3. 高手追小萝莉的故事(洛谷P1184题题解,Java语言描述)
  4. Valid Number 1
  5. 亮屏变“黄”,暗屏变“绿”,iPhone 12用户太难了
  6. [翻译] Canvas 不用写代码的动画
  7. 9篇前沿文章 | 一览肿瘤基因组及多组学思路
  8. 完成网络传真,网络扫描。
  9. 微信公众号(头部GIF动图)制作方法
  10. 广告算法,反作弊,机器学习研发工程师
  11. 前端开发——在线工具推荐
  12. 亥姆霍兹线圈分类简介
  13. 18 个坏习惯,你一定要抛弃
  14. k8s探针检测php,K8S教程(7)使用探针对容器进行健康检查
  15. 20175312 2018-2019-2 实验三 敏捷开发与XP实践 实验报告
  16. C语言创建24位真彩色位图
  17. Mann-Whitney U检验
  18. eDP转LVDS转换器|DP转LVDS转接板方案|eDP转LVDS控制板方案设计|可替代兼容 PS8622 PS8625 CH7511方案
  19. 更丰富的云原生应用治理能力让业务快速生长
  20. count case when 与sum case when 的 区别

热门文章

  1. 2022-2028年中国汽车修理行业市场前瞻与投资规划分析报告
  2. 2022-2028年中国商业综合体行业市场前瞻与投资规划分析报告
  3. Go 知识点(17)— go 工具链 go test 使用
  4. RSA、MD5等加密算法的区别和应用
  5. 【读书笔记】知易行难,多实践
  6. ionic4中实现时间线
  7. 办公word,ppt,excel问题
  8. 嵌入式Linux设备驱动程序:编写内核设备驱动程序
  9. 2021年大数据Flink(二十四):​​​​​​​Allowed Lateness案例演示
  10. 2021年大数据Flink(十六):流批一体API Connectors ​​​​​​​​​​​​​​Redis