Go in Action 读书笔记 二

Go In Action 读书笔记 二

Go语言的类型系统

Go语言是静态类型的变成语言. 编译的时候需要确定类型.

用户定义的类型

type user struct {
    name    string
    email   string
    ext     int
    privileged  bool
}

使用 零值和结构字面量初始化

//引用类型, 各个字段初始化为对应的零值
var bill user #{  0 false}
//创建并初始化, 使用结构字面量
lisa := user{ //{Lisa lisa@email.com 123 true}
    name: "Lisa",
    email: "lisa@email.com",
    ext: 123,
    privileged: true,
}

结构字面量的赋值方式:

  • 不同行声明每一个字段和对应的值, 字段名和字段以:分隔, 末尾以,结尾
  • 不适用字段名, 只声明对应的值. 写在一行里, 以,分隔, 结尾不需要,. 要保证顺序
lisa := {"Lisa", "lisa@email.com", 123, true}

使用其他类型结构声明字段

type admin struct {
    person  user
    level   string
}

fred := admin{ //{{Fred fred@email.com 123 true} super}
    person: user{
        name: "Fred",
        email: "fred@email.com",
        ext: 123,
        privileged: true,
    },
    level: "super",
}

另一种声明用户定义的类型的方法是, 基于一个已有的类型, 将其作为新类型的类型说明 新的类型是独立的类型, 值互相兼容, 但不能互相赋值.

type Duration int64

var d Duration
//d = int64(1000) #编译错误cannot use int64(1000) (type int64) as type Duration in assignment
d = Duration(1000)

方法

描述用户自定义类型的行为, 实际为函数. 只是在声明的时候在func和方法名之间增加了一个参数(接收者), 将函数和接收者的类型绑定到一起.

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 main() {
	bill := user{"Bill", "bill@email.com"}
	bill.notify()

	lisa := &user{"Lisa", "lisa@email.com"}
	lisa.notify() //实际执行 (*lisa).notify()

	bill.changeEmail("bill@newDomain.com") //实际执行 (&bill).changeEmail("bill@newDomain.com")
	bill.notify()

	lisa.changeEmail("lisa@newDomain.com")
	lisa.notify() //实际执行 (*lisa).notify()
}
//Sending User Email To Bill<bill@email.com>
//Sending User Email To Lisa<lisa@email.com>
//Sending User Email To Bill<bill@newDomain.com>
//Sending User Email To Lisa<lisa@newDomain.com>

Go语言里有两种类型的接收者: 值接收者和指针接收者.

  • 如果使用值接收者, 调用的时候会使用值的副本来执行
  • 如果使用指针接收者, 调用的时候这个方法会共享调用方法时接收者所指向的值

类型的本质

声明类型的方法前要确定该方法是创建一个新值(使用值接收者), 还是修改当前值(使用指针接收者)

内置类型

由语言提供: 数值类型, 布尔类型, 字符串类型. 本质上是原始类型. 对这些值增加或删除操作的死后, 都会创建新的值.

golang.org/src/strings/strings.goTrim函数传入字符串值, 返回新的字符串.

func Trim(s string, cutset string) string {
	if s == "" || cutset == "" {
		return s
	}
	return TrimFunc(s, makeCutsetFunc(cutset))
}

引用类型

Go语言里有几种: 切片, 映射, 通道, 接口和函数类型.

声明上述类型的变量时, 创建的变量被称作标头(header)值. 每个引用类型创建的标头值是包含一个指向底层数据结构的指针. 标头值里包含一个指针, 通过复制来传递一个引用类型的值得副本, 本质是就是在共享底层数据结构.

golang.org/src/net/ip.go

type IP []byte

结构类型

描述一组数据值, 这组值可以是原始类型, 也可以是非原始的. 结构类型的本质是非原始的. 对这个类型的值做增加或者删除的操作应该更改值本身. 当需要修改值本身时, 在程序中其他地方, 需要使用指针来共享这个值.

golang.org/time/time.go

type Time struct{
    wall uint64
    ext  int64
    loc *Location
}

func Now() Time{
    ...
}
//值接收者, 返回新的Time
func (time Time) Add(d Duration) Time{
    ...
}

func (time Time) String() string{
    ...
}
//指针接收者
func (t *Time) UnmarshalBinary(data []byte) error {
    ...
}

如果一个创建用的工厂函数返回了一个指针, 就表示这个被返回的值的本质是非原始的. golang.org/src/os/file.goopen函数.

type File struct {
    *file //内嵌类型: 嵌入的指针, 指向一个未公开的类型
          //一种保护的方式
}

type file struct {
	pfd      poll.FD
	name     string
	dirinfo  *dirInfo // nil unless directory being read
	nonblock bool     // whether we set nonblocking mode
}

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    ...
}

接口

多态是指代码可以根据类型的具体实现采取不同行为的能力. 如果一个类型实现了某个接口, 所有使用这个接口的地方, 都可以支持这种类型的值.

标准库

golang.org/src/io/io.go

type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}
type WriterTo interface {
	WriteTo(w Writer) (n int64, err error)
}

func main() {
//bytes.Buffer实现了io.Reader, io.WriteTo接口
	var b bytes.Buffer
	b.Write([]byte("Hello"))
	fmt.Fprintf(&b, "World!")
//os.Stdout实现了io.Writer接口	
	io.Copy(os.Stdout, &b)
}

实现

接口是定义行为的类型, 具体的实现由用户定义的类型完成. 用户定义的类型通常称作实体类型. 如果用户定义的类型实现了某个接口类型声明的一组方法, 那么这个用户定义的类型的就可以赋给这个接口类型的. 这个赋值会把用户定义的类型的存入接口类型的.

接口的值是一个两个字长度的数据结构:

  • 第一个字包含一个指向内部表(iTable)的指针. 内部表包含了所存储的值的类型信息, 还包含了与这个值相关联的一组方法.
  • 第二个字是一个指向所存储的值的指针.

这部分可以参考Laws of Reflecation

方法集

方法集定义了接口的接受规则. 方法集定义了一组关联到给定类型的值或者指针的方法. 定义方法的时使用的接收者的类型决定了这个方法是关联到值还是关联到指针, 还是两个都关联.

type notifier interface {
	notify()
}

type user struct {
	name  string
	email string
}
//notify是使用指针接收者实现的方法
func (u *user) notify() {
	fmt.Printf("Send email to %s<%s>\n", u.name, u.email)
}

func main() {
	u := user{"Bill", "bill@email.com"}
	//sendNotificationTo(u) //用这一行会有编译错误. user没有实现notifier接口, 赋值给notifier会发生错误
	sendNotificationTo(&u) //上面notify方法的实现的接收者为 user指针, 因此在赋值的时候只能接受user指针
	                       //或者上面方法实现的接收者改为user
}
//接受一个实现了notifier的值作为参数
func sendNotificationTo(n notifier) {
	n.notify()
}

Go语言规范里定义的方法集的规则:

ValuesMethods Receiver
T(t T)
*T(t T) and (t *T)

T类型的值的方法集只包含值接收者声明的方法. 而指向T类型的指针的方法集既包括指针接收者声明的方法, 也包含值接收者声明的方法.

上面的代码稍微做下修改, 更加清晰一些.

type notifier interface {
	notify()
}

type user struct {
	name  string
	email string
}
//notify是使用值接收者实现的方法
func (u user) notify() {
	fmt.Printf("Send email to %s<%s>\n", u.name, u.email)
}

func main() {
	u := user{"Bill", "bill@email.com"}
	sendNotificationTo(u)
	sendNotificationTo(&u) //&u赋值给notifier的变量n时, n的方法集包含了值接收者实现的方法.
}

func sendNotificationTo(n notifier) {
	n.notify()
}

或者换个角度, 从接收者来看.

Method ReceiverValue
(t T)T and *T
(t *T)*T

使用指针接收者实现的接口, 那只有指向那个类型的指针才能实现对应的接口. 使用值接收者实现的接口, 那么那个类型的值和指针都能够实现对应的接口.

多态

上面的函数sendNotificationTo其实就是一个多态函数.

嵌入类型

type embedding, Go语言允许用户扩展或者修改已有类型的行为. 可用于代码复用, 或修改已有类型以符合新类型. 嵌入类型是将已有类型直接声明在新的结构类型里. 被嵌入的类型称为新的外部类型的内部类型.

通过嵌入类型, 与内部类型相关的标识符会提升到外部类型上, 也成为外部类型的一部分. 外部类型也可以通过声明相同名称的标识符来覆盖内部类型的标识符的字段或者方法, 这就是修改内部类型的属性或者行为实现. 也可以添加新的字段和方法.

type notifier interface {
	notify()
}

type user struct {
	name  string
	email string
}

type admin struct { //外部类型
	user //内部类型
	level string
}

func (u *user) notify() {
	fmt.Printf("Send email to %s<%s>\n", u.name, u.email)
}

func main() {
	ad := admin {
		user: user{
			name: "John",
			email: "john@email.com",
		},
		level: "super",
	}
	
	ad.user.notify() //可以直接访问内部类型的方法
	ad.notify() //内部类型的方法也被提升到外部类型
	endNotificationTo(&ad) //由于内部类型的提升, 内部类型实现的接口也被提升到外部类型. 外部类型也可以提供同名的方法实现, 以达到覆盖的效果.
}

func sendNotificationTo(n notifier) {
	n.notify()
}

如果外部类型做了方法覆盖, 对内部类型方法的访问也还是会继续执行内部类型的方法

func (ad *admin) notify() {
    ...
}
ad.user.notify() //执行内部类型的方法
ad.notify() //执行外部类型的方法

公开或未公开的标识符

使用规则来控制声明后的标识符的可见性. Go语言支持从包里公开或者隐藏表示. 这里的标识符包括类型, 变量, 方法. 当一个标识符的名字是小写开头的时候, 这个标识符就是未公开的. 如果是大写字母开头就是公开的, 包外的代码可见.

package user
type User struct{
    Name string //公开字段
    email string //未公开字段
}
//构造器
func New(name, email string) User {
    return user{name, email}
}

---------------
package another
//在另一个包里使用User类型
u := user{
    Name: "Bill",
    email: "bill@email.com", //编译器报错, 找不到email字段. 因为email字段未公开
}
//使用构造器
ur : user.New("Bill", "bill@email.com")
  • 公开或者未公开的标识符, 不是一个值
  • 短变量声明操作符(:=), 有能力捕获引用的类型, 并创建一个未公开的类型的变量.
package counter
type alertCounter int //未公开类型
---------------

package another
import (
    "fmt"
)
func main() {
    //c = counter.alertCounter 编译会报错, 无法访问未公开标识符
    c := counter.alertCounter(20)
    fmt.Println(counter) // 20
}

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus