一、用户定义的类型
Go语言里声明用户定义的类型有两种方法。最常用的方法是使用关键字struct,它可以让用户创建一个结构类型。结构里每个字段都会用一个已知类型声明。这个已知类型可以是内置类型,也可以是其他用户定义的类型。
1. 结构型类型
//user 在程序里定义一个用户类型
type user struct {
name string
email string
ext int
privileged bool
}
var bill user //声明user类型的变量
//声明user类型的变量,并初始化所有字段
lisa := user {
name : "Lisa",
email : "lisa@email.com",
exit : 1,
privileged : true,
}
lisa := user{"Lias", "lisa@email.com", 123, true} //不使用字段名,创建结构类型的值
//使用其他结构类型声明字段
type admin struct {
person user
level string
}
//声明admin类型的变量
fred := admin {
person: user {
name : "Lisa",
email : "lisa@email.com",
exit : 1,
privileged : true,
},
level : "super"
}
2. 基于已有类型的新类型
另一种声明类型的方法是基于一个已有的类型,将其作为新类型的类型说明。当需要一个可以用已有类型表示的新类型的时候,这种方法比较好用。
//基于int64声明一个新类型
type Duration int64
package main
type Duration int64
func main() {
var dur Duration
dur = int64(1000)
}
二、方法
方法能够用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字func和方法名之间增加了一个参数。
package main
import (
"fmt"
)
//定义的一个用户类型
type user struct {
name string
email string
}
//notify使用值接受者实现了一个方法
func (u user) notify() {
fmt.Printf("Sending User Email to %s<%s>\n",u.name, u.email)
}
//changeEmail使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
u.email = email
}
func main() {
bill := user{"Bill", "bill@email.com"}
bill.notify() //值调用
lisa := &user{"Lisa", "lisa@email.com"}
bill.notify() //指针调用
bill.changeEmail("bill@newdomain.com")
bill.notify()
lisa.changeEmail("lisa@comcast.com")
lisa.notify()
}
三、类型的本质
程序内部传递这个类型的值的方式:是按值做传递还是按指针做传递。保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值得,而是要关注这个值得本质是什么。
1. 内置类型
内置类型是由语言提供的一组类型。分别是数值类型、字符串类型和布尔类型。这些类型本质上是原始的类型。因此,当对这些值进行增加或者删除的时候,会创建一个新值。基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
func Trim(s string, cutset string) string {
if s == "" || cutset == "" {
return s
}
return TrimFunc(s, makeCutsetFunc(cutset))
}
Trim函数传入一个string类型的值做操作,再传入一个string类型的值用于查找。之后函数会返回一个新的string值作为操作结果。这个函数对调用者原始的string值得一个副本做操作,并返回一个新的string值得副本。字符串就像整数、浮点数和布尔值一样,本质上是一种很原始的数据值,所以在函数或方法内外传递时,要传递字符串的一份副本。
2. 引用类型
Go语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型的变量时,创建的变量被称作标头值。从技术细节上说,字符串也是一种引用类型。每个引用类型创建的标头值是包含一个指向底层数据结构的指针。每个引用类型还包含一组独特的字段,用于管理底层数据结构。因为标头值是为复制而设计的,所以永远不需要共享一个引用类型的值。标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。
32 type IP []type
func (ip IP) MarshalText() ([]byte, error) {
if len(ip) == 0 {
return []type(""),nil
}
if len(ip) != IPv4len && len(ip) != IPv6len {
return nil, errors.New("invaild IP address")
}
return []byte(ip.String()),nil
}
MarshalText方法是用IP类型的值接收者声明的。一个值接收者,正像预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。这种传递方法也可以应用到函数或者方法的参数传递。3. 结构类型
结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的。如果决定在某些东西需要删除或者添加某个结构类型的值时该结构类型的值不应该被更改,那么需要遵守之前提到的内置类型和引用类型的规范。下面是从标准库里的一个原始本质的类型的结构实现开始:
type Time struct {
sec int64
nsec int32
loc *Location
}
//Now函数创建Time类型的值
func Now() Time {
sec, nsec := now()
return Time{sec + unixToInternal, nsec, Local}
}
//下面是一个Time类型的方法
func (t Time) Add(d Duration) Time {
t.sec += int64(d / 1e9)
nsec := int32(t.nsec) + int32(d%1e9)
if nsec >= 1e9 {
t.sec++
nsec -= 1e9
} else if nsec < 0 {
t.sec--
nsec += 1e9
}
t.nsec = nsec
return t
}
大多数情况下,结构类型的本质并不是原始的,而是非原始的。在这种情况下,对这个类型的值做增加或者删除的操作应该更改值本身。当需要修改值本身时,在程序中其他地方,需要使用指针来共享这个值。下面是标准库中实现的具有非原始本质的结构类型的例子:
//File表示一个打开的文件描述符
type File struct {
*file
}
//file是*File的实际表示,额外的一层结构保证没有那个os的客户端能够覆盖这些数据
//如果覆盖这些数据,可能在变量终结时关闭错误的文件描述符
type file struct {
fd int
name string
dirinfo *dirInfo
nepipe int32
}
File类型的本质是非原始的。这个类型的值实际上不能安全复制。对内部未公开的类型注释,解释了不安全的原因。因为没有办法阻止程序员进行复制,所以File类型的实现使用了一个嵌入的指针,指向一个未公开的类型。
下面是Open函数的实现
func Open(name string) (file *File, err error) {
return OpenFile(name, O_RDONLY, 0)
}
调用者得到的是一个指向File类型值得指针。Open创建了File类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值得本质是非原始的。
即便函数或者方法没有直接改变非原始的值得状态,依旧应该使用共享的方式传递,如下所示
func (f *File) Chdir() error {
if f == nil {
return ErrInvalid
}
if e := syscall.Fchdir(f.fd); e != nil {
return &PathError{"chdir", f.name, e}
}
return nil
}
即使没有修改接收者的值,依然是用指针接收者来声明的。因为File类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。