Go是一种静态的语言,所以需要在编译的时候获取到每个值的类型
编译器可以利用类型信息来合理的使用值,减少潜在的异常的bug
值的类型有两部分决定,一个是值的内存规模,二是值的类型
那么在Go中,我们是否可以直接定义一种类型呢?
对于用户来说,可以自定义类型,用户声明一个新的类型的时候,这个声明就会让编译器去计算得到必要的内存大小和表示信息
对于用户的类型的声明
有两种方式
1.使用关键字 struct
声明如下
type user struct {
name string
email string
ext int
privileged bool
}
上面的结构类型有4个字段,每个都是一个内置的类型,一旦声明了类型,就可以使用这个类型创建值
var heaven user
创建了一个名为heaven的user类型变量,这时候类型中所有的值,都会利用零值来进行初始化
对于数值类型来说,0为零值,对字符串来说,空字符串为零值,布尔类型,false是零值
或则是在声明的时候直接设置对应的变量
我们使用某个非零值作为初始化来进行,可以将代码设置如下,我们使用短变量声明操作符
:= 这种方式来完成操作,声明一个变量,并且进行初始化,这样就是初始化了所有字段
lisa := user{
name: “Lisa”
email: “lisa@1.com”
ext: 123
privaileged: true
}
我们对于结构类型的声明,我们使用了第一种的方式,直接指定对应的键和值
然后我们还可以使用另一种方式,没有字段名,只声明对应的值
代码如下
lisa := user{“Lisa”,”lisa@1.com”,123,true}
每个值,顺序需要和字段的顺序保存一致
当然,我们支持类型套类型
我们可以在其中在设置一个user类型作为管理者,并附加权限
type admin struct {
person user
level string
}
这样我们创建了一个新的结构类型
结构类型中有一个名为person的user类型字段,还有一个名为level的string字段
创建person的时候,代码块如下
fred := admin {
person: user{
name : “Lisa”,
email: “lisa@1.com”
ext: 123,
privileged:true,
},
level: “super”,
}
初始化person字段,我们就创建了一个user类型的值,赋予给了person字段
基于这种思想,我们可以利用内置类型来创建出更多明确的类型,赋予更加高级的功能
比如一个简单的int
我们将其称为
type Duration int64
Duration就是标准库中的time包的一个生命 Duration描述了时间间隔
默认单位是纳秒的,使用了内置的int64类型进行表示
虽然利用了int作为本质的原型
但是Go并不认为这两个是同一种类型,这两种是完全不同的类型
区别比如下面的代码
我们声明了Duration类型,然后使用int来给duration来赋值
这样导致了代码编译的错误,因为go中int64不能直接作为Duration来使用
对于方法
我们可以利用方法,给用户自定义的类型增加方法,方法也就是函数
声明方式如下
type user struct {
name string
email string
}
我们定义类型的一个方法
func(u user) notify(){
fmt.Printf(“Sending User Email To %s<%s>\n”,u.name,u,email)
}
并利用指针来实现了一个方法
func (u*user) changeEmail(email string){
u.email = email
}
我们就创建了两个方法,将函数和类型绑定在一起,如果一个函数有接受者,就被称为方法
定义就是在func和函数名之间的参数,那地方的参数也被称为接收者
Go语言中的接受者,值接收者 和 指针接收者
无论使用那种方式来进行接收,都是可行的
比如我们声明了上面的notify方法
我们利用一个user类型的变量来进行调用就可以了
bill := user{“Bill”,”bill@email.com”}
bill.notify()
或者我们可以使用指针来调用这个方法
lisa := &user{“Lisa”,”lisa@email.com”}
lisa.notify()
使用指针来调用,是Go内部将其调整为了指针,来付符合方法接受者的定义
(*lisa). notify()
notify操作的,本质上就是lisa指针指向的值的副本
也可以使用指针接受者声明方法
func (u *user) changeEmail(email string){
u.email = email
}
展示了changeEmail方法的声明
说明了使用指针来接收者,而不是直接使用类型
通常来说调用如下
lisa := &user{“Lisa”,”lisa@email.com”}
lisa.changeEmail(“lisa@newdomain.com”)
对于接收者是指针的标准调用上
但是如果使用一个值来进行调用,也是掉的通的
bill := user{“Bill”,”bill@email.com”}
bill.changeEmail(“bill@newdomain.com”)
这样也是掉的通的,只不过Go对其进行了隐性的调整
(&bill).changeEmail(“bill@newdomain.com”)
将其转换为指针,来进行调用调用方法
对于声明了一个新的类型之后,我们选择是利用指针来调用还是利用原本类型呢?
这个问题可以无限趋近于,这个类型是创建一个新的值还是要改变当前的值呢?如果要创建一个新值,那么可以考虑使用值来作为接收者,如果是修改当前值,那么使用指针作为接收者,这可以作为我们考虑传递元素的一个原则
内置类型
是Go本身提供的一组类型,分别是数值类型,字符串类型,布尔类型
原始的类型,所以对于这些值的操作,都是新建而不是修改
对于这种内置的类型,在传递的时候,一般是传递的对应值的副本
我们看一下函数传递的代码
上面代码,我们传入两个string
一个用于查找,一个用于操作,之后返回一个新的string作为操作结果
另一个查看内置类型的例子
上面就是传入了一个int类型的副本,返回一个值 true或者false
引用类型
GO中自带的引用类型有如下几个 切片 映射 通道 接口 函数类型,声明引用类型的时候,创建一个标头,每个引用类型创建的标头值是包含一个指向底层数据结构的指针,方便管理底层的数据,这些标头值的复制相对轻松,所以使用标头值来复制传递副本,可以方便共享底层数据结构
例如一个类型 IP
type IP []byte
这个类型本质上是一个字节的切片
但是go并不认为这是一个字节切片,而且这种定义方式,是被允许的,编译器支持为命名的用户定义的类型声明方法
我们定义的MarshalText就是用IP类型的值接收者声明的,一个值接收者,可以通过复制来传递引用,也不需要利用指针来进行共享引用类型的值
然后是我们的结构类型
先看一个实例
type Time struct {
sec int64
nsec int32
loc *time.Location
}
对于这样一个Time,我们如何进行创建的呢?
我们看一下Time包中的Now函数如何创建的
我们创建了一个Time类型的值,并给调用者返回了一个Time类型的值,直接返回了一个副本,没有使用指针来返回的,毕竟这是一个倾向于新建的一个操作
然后是Time类型的方法
我们传入了利用一个Time作为接收者,返回了一个新的Time的值,操作的是调用者传入的Time值的副本,并且返回了一个方法内的Time值的副本
很多的时候,结构类型的本质并不是原始的,而是非原始的,这时候,对这个类型的值做增加或者删除的操作需要更改值的本身,所以我们有些时候需要强制使用指针来共享这个值
比如在File这个结构
File类型由于打开了磁盘文件,所以不能安全的赋值,而且们对于这种方法,没有阻止程序员复制,为了避免复制这种危险操作,所以File利用一个指针,指向了内部一个未公开的类型,从而保护并阻止复制
而且,为了增加保护的程度
Open函数返回的是一个File的指针
说明返回的值并非原始的
让所有的使用者共享这个函数
而且File相关的函数,也是指定的指针作为接收者的
即时没有修改接收者的值,还是使用了指针作为接收者,因为File应该被共享,而不是被复制
接口值
Go中同样支持多态,根据类型的具体实现采用不同的行为,一个类型实现了某个接口,所有使用这个接口的地方,都支持这种类型的值
常见的,诸如io包中的流式处理接口,提供了一组构造很好的接口和函数,让其支持流式数据处理,我们先拿标准库中的代码看一下接口的功能
func init() {
if len(os.Args) != 2 {
fmt.Println(“Usage: ./example2<url>”)
os.Exit(-1)
}
}
func main() {
get, err := http.Get(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
//拷贝输出
io.Copy(os.Stdout,get.Body)
if err :=get.Body.Close(); err != nil {
fmt.Println(err)
}
}
上述代码中,我们利用http包中的Get函数,获取到了返回的http.Response类型指针,内部包含一个body的字段,为io.ReadCloser接口类型的值
然后利用io.Copy函数,我们接受了一个ioReader类型的值,Body实现了这个接口,可以传入io.Copy,进行拷贝
ioCopy还需要一个参数,一个实现了io.Writer接口的参数,所以我们使用了os的stdout作为标准的输出设备,这个stdout实现了io.Wrrter接口
我们看一个用Buffer来实现输出的方式
我们使用流的方式,缓存获取数据,然后进行定时的输出
我们创建了一个bytes包中的Buffer,然后进行写入字符串,然后输出到终端窗口
这样我们看完了接口的好处,我们看下如何实际的实现接口
接口是用于定义行为的类型,这些类型不使用接口直接实现,而是交由用户定义的类型去实现,用户定义的类型实现接口声明的一组方法,这个用户自定义类型就可以在赋值的时候作为接口类型
对于接口值方法的调用,会执行接口值里存的用户类型的方法,这就是一种多态
用户定义的类型叫做实体类型
比如我们创建了一个user类型的变量,user类型赋值后接口变量布局如下
这个user类型有两个指针
第一个指针指向了内部表的指针,内部表称为iTable,包含了所存储的值的类型信息
iTable包含了已经存储的值的类型信息以及这个值相关的一组方法
第二个指向所存储的值的指针,将类型信息和指针组合在一起
然后就iTable,这是一个方法集,定义了接口的接收规则
我们先看一串代码
package main
import (
“fmt”
)
// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}
// user defines a user in the program.
type user struct {
name string
email string
}
// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
fmt.Printf(“Sending user email to %s<%s>\n”,
u.name,
u.email)
}
// admin represents an admin user with privileges.
type admin struct {
user
level string
}
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: “john smith”,
email: “john@yahoo.com”,
},
level: “super”,
}
// Send the admin user a notification.
// The embedded inner type’s implementation of the
// interface is “promoted” to the outer type.
sendNotification(ad)
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
上面的代码中,看起来没问题
声明了一个接口,声明了对应的实体类型,实现了对应的接口
声明了一个名为sendNotification的函数,接收一个notifier接口类型的值,然后调用对应的notify函数
但是我们上述代码的调用会带来一个编译时报错的问题
报错信息为 user类型并没有实现notifier
这就牵扯到了接口编译的问题了
方法集定义了一组关联到给定类型的值或者指针得到方法,定义方法时候使用接受者的类型决定了这个方法是关联到值还是关联到指针,还是两个都关联
Go语言中定义方法集的规则如下
从另一个角度来看
我们从方法的接受者来看
如果声明为了指针,那么接受者只能是指针
如果是实现者,那么这个类型的值或者指针都能实现对应的代码
于是,我们利用指针的接受者实现了接口,但是试图将user的类型的值传给了sendNotication,导致的问题,如果尝试传递的是user值的地址,或者说指针,就能正常工作了
这也是Go编译器的一种限制,并不能自动的获得一个值的地址
多态
我们看一下接口的多态行为的例子
我们看一下接口多态行为的例子
package main
import (
“fmt”
)
// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}
// user defines a user in the program.
type user struct {
name string
email string
}
// notify implements a method that can be called via
// a value of type user.
func (u user) notify() {
fmt.Printf(“Sending user email to %s<%s>\n”,
u.name,
u.email)
}
// admin represents an admin user with privileges.
type admin struct {
user
level string
}
// notify implements a method that can be called via
// a value of type Admin.
func (a admin) notify() {
fmt.Printf(“Sending admin email to %s<%s>\n”,
a.name,
a.email)
}
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: “john smith”,
email: “john@yahoo.com”,
},
level: “super”,
}
// Send the admin user a notification.
// The embedded inner type’s implementation of the
// interface is NOT “promoted” to the outer type.
sendNotification(ad)
// We can access the inner type’s method directly.
ad.user.notify()
// The inner type’s method is NOT promoted.
ad.notify()
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
分别定义了admin和user的结构,同样的都实现了notifier接口,然后分别执行sendNotification,两者都可以执行的通
然后是嵌入类型
Go语言允许用户扩展或者修改已经有的类型的行为,修改已有类型符合新类型的对代码复用很重要,这个是通过嵌入类型完成的
嵌入类型,于内部类型相关的标识符会提升到外部类型上,这些被提升的标识符就好比直接声明在外部类型上,也是外部类型的一部分,这样外部类型组合了内部类型包含的属性,可以添加新的字段和方法,外部类型可以通过声明和内部类型标识符相同的标识符来覆盖内部标识符的字段和方法,扩展和修改现有类型的方法
还是一样的代码
package main
import (
“fmt”
)
// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}
// user defines a user in the program.
type user struct {
name string
email string
}
// notify implements a method that can be called via
// a value of type user.
func (u user) notify() {
fmt.Printf(“Sending user email to %s<%s>\n”,
u.name,
u.email)
}
// admin represents an admin user with privileges.
type admin struct {
user
level string
}
/*// notify implements a method that can be called via
// a value of type Admin.
func (a admin) notify() {
fmt.Printf(“Sending admin email to %s<%s>\n”,
a.name,
a.email)
}*/
// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: “john smith”,
email: “john@yahoo.com”,
},
level: “super”,
}
// Send the admin user a notification.
// The embedded inner type’s implementation of the
// interface is NOT “promoted” to the outer type.
sendNotification(ad)
// We can access the inner type’s method directly.
ad.user.notify()
// The inner type’s method is NOT promoted.
ad.notify()
}
// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}
我们在admin中内嵌了一个类型,对于新内嵌的类型,我们可以说user是外部类型admin的内部类型
内部类型和外部类型两个概念之后,就能理解这两种类型的关系了
上述代码如果admin不实现notify方法
也是可以调用的,因为内部类型实现了对应的notify方法,访问到内部类型的值
admin没有实现这个接口,但是由于内部类型的提升,内部类型实现的接口会自动的提升到外部类型,内部类型的实现,外部类型也实现了这个接口
如果外部类型自己也实现这个接口,那么会先调用外部的接口
但是内部类型的实现虽然不会提升,但是类型的值一直存在,所以可以通过直接访问内部类型的值,来调用没有提升的内部类型的方法
标识符
公开或者未公开的
Go支持从包中公开或者隐藏标识符
用户可以按照自己的规则来控制标识符的可见性
如果不希望公开包中的某个类型,函数或者方法这种标识符,就需要将其生命为不可见的,未公开的
我们看下如何隐藏包中未公开的标识符
因为不能引用未公开的名字
所以飘红报错了
一个标识符的名称以小写字母开始的时候,这个标识符就是未公开的,包外不可见的,如果一个标识符以大写字母开头,就是公开的,可见的
但是我们可以利用工厂函数来暴露出来
// Package counters provides alert counter support.
package counters
// alertCounter is an unexported type that
// contains an integer counter for alerts.
type alertCounter int
// New creates and returns values of the unexported
// type alertCounter.
func New(value int) alertCounter {
return alertCounter(value)
}
利用一个New的工厂函数,返回这个alterCounter
我们利用工厂函数来创建一个未公开的alterCounter类型的值
返回给了名为counter的变量
上述的创建是一种隐式的创建方式
当然直接创建公开的是没有问题的
// Package entities contains support for types of
// people in the system.
package entities
// User defines a user in the program.
type User struct {
Name string
email string
}
u := entites.User{
Name: “Bill”,
email: “bill@email.com”,
}
我们导入了包并进行了初始化
最后看一个外部类型裹挟内部类型的例子
我们首先声明两个结构
声明了一个未公开的类型user和一个公开的类型Admin,Admin中内嵌一个未公开的类型user
在main函数中,
我们创建了一个Admin,内部声明了一个公开的字段,对于user这个未公开的类型,我们是无法访问的,但是公开的字段可以进行访问
本次小结一下
关键字struct 可以通过指定已经存在的类型,声明用户定义的类型
方法提供了一种给用户定义的类型增加行为的方式
设计类型的时候需要确认类型的本质是原始的还是非原始的
接口是一种声明了一组行为并支持多态的类型
嵌入类型提供了扩展类型的能力,无须使用继承