10.保密:封装和嵌入

Monday, August 7, 2023

有时,程序会接收到无效的数据,从用户输入、文件读取或其他方式。封装:一个保护string字段免受无效数据的方法。这样,数据字段能够安全使用。
也会在类型内部嵌入其他的类型,若struct类型需要已经存在于其他类型的方法,不需要赋值方法代码,可以将其嵌入struct类型中,然后像使用自己的类型的定义方法一样使用嵌入的方法。

创建一个日期struct类型

在struct中增加Year、Month、Day字段,每个都是int类型,在main函数中,将执行一个快速的测试来测试新类型,使用struct字面量来创建一个Date值,并填充所有字段。

package main

import "fmt"

type Date struct {
        Year  int
        Month int
        Day   int
}

func main() {
        date := Date{Year: 2023, Month: 8, Day: 10}
        fmt.Println(date)
}

将Date struct字段设置为无效值

用户将日期格式设置的很奇怪,比如:“2023 14 50” 或 “0 0 -2” 等,我们知道只有1以上的年的数字才合法,但程序没做任何措施预防用户把日志以外设置为0或-999,月份也只有1-12合法,日只有1-31合法。

需要一种方法,让用户数据在被赋值之前是合法的,在CS领域中,被称为数据校验,需要测试Year被设置为大于或等于1,Month被设置为1-12,Day被设置为1-31。

setter方法

一个struct类型就是另一个定义的类型,可以像对其他类型一样定义它的方法。可以在Date类型上创建 SetYear、SetMonth、SetDay方法来接收值,判断是否有效,若有效则设置到struct字段。

这类方法被称为 setter方法 , Go的setter方法名为SetX的形式,X 是想要设置的名称。

setter 方法 是用来设置字段或基础类型中的其他值的方法。

在SetYear方法上进行第一次尝试,接收器参数是Date struct,SetYear接受想要设置的年份作为参数,并设置接收器Date struct上的Year字段,此时并不校验值有效性。在main方法中创建Date并调用SetYear,然后输出struct的Year字段。

package main

import "fmt"

type Date struct {
        Year  int
        Month int
        Day   int
}

func (d Date) SetYear(year int) {
        d.Year = year    //设置struct字段
}

func main() {
        date := Date{}  //创建一个Date
        date.SetYear(2023)  //通过方法设置Year字段
        fmt.Println(date.Year)  //打印Year字段
}

输出:

0

当执行时,并没有正常工作,创建了一个Date并使用新值调用了它的setYear方法,Year字段仍被设置为零值。

setter方法需要指针接收器

之前Number类型上的Double方法,使用了一个普通的值接收器类型Number,像其他参数一样,接收器参数接受了一个原值的拷贝,Double方法只是更新了拷贝,在方法退出时更新就丢失了。

func (n *Number) Double() {   //接收器类型为一个指针类型
        *n *= 2  //修改指针指向的值
}

需要更新Double来接受指针的接收器类型 *Number ,当更新指针指向的值的时候,在Double退出后更新会保留。

同样,SetYear也适用,Date接收器从原struct获取了一个拷贝,任何字段更新都在setYear退出后消失。

通过将接收器的值修改为指针来修正,不需要更新SetYear方法块,因为 d.Year 会自动获取指针指向的值(与(*d).Year 一样),main函数中的date.SetYear也不需要修改,因为当Date值传递给方法的时候,会自动转换为 *Date

type Date struct {
        Year  int
        Month int
        Day   int
}

func (d *Date) SetYear(year int) {
        d.Year = year
}

func main() {
        date := Date{}
        date.SetYear(2023)  //自动转换为指针
        fmt.Println(date.Year)
}

输出:

2023

添加其余的setter方法

按照这个模式在Date类型上定义SetMonth和SetDay方法,只需要在方法中使用指针接收器即可,在调用方法时,Go会自动把接收器转换为指针,并在修改struct字段时能够把指针转换回struct值。

package main

import "fmt"

type Date struct {
	Year  int
	Month int
	Day   int
}

func (d *Date) SetYear(year int) {
	d.Year = year
}

func (d *Date) SetMonth(month int) {
	d.Month = month
}

func (d *Date) SetDay(day int) {
	d.Day = day
}

func main() {
	date := Date{}
	date.SetYear(2023)
	date.SetMonth(5)
	date.SetDay(27)
	fmt.Println(date)  //输出所有字段
}

在main函数中,创建了一个Date struct的值,通过新的方法设置了它的Year、Month和Day字段,并输出了整个struct查看结果。

在setter方法中添加校验

在每个setter方法中,将测试值是否在正确合法范围内,若非法,返回error值,若合法,将正常设置 Date struct字段,并返回nil作为错误值。

首先对SetYear方法增加校验,修改方法的声明,增加一个error类型的返回值,在方法块的开始处,测试调用者传入的year参数是否小于1,若小于1,返回一个error信息,若大于等于1,将其设置为struct的Year字段并返回nil,表示没有错误。

在main函数中,调用SetYear并把返回值保存到名为err的变量中,若err不为nil,意味着赋值无效,所以记录错误并退出,否则继续输出Date struct的Year字段。

package main

import (
	"errors" //允许创建error值
	"fmt"
	"log" //允许记录error并退出
)

type Date struct {
	Year  int
	Month int
	Day   int
}

func (d *Date) SetYear(year int) error { //增加一个error类型的返回值
	if year < 1 {
		return errors.New("invalid year") //若year是非法的,返回错误
	}
	d.Year = year
	return nil
}

func main() {
	date := Date{}
	err := date.SetYear(0) //抓捕任何错误
	if err != nil {
		log.Fatal(err)  //若值无效记录错误并退出
	}
	fmt.Println(date.Year)
}

输出:

2023/08/10 16:56:33 invalid year
exit status 1

传入一个无效的值给SetYear导致程序报错并退出,但若传入有效值,程序会继续输出,SetYear方法工作正常。

SetMonth 与 SetDay 方法中的校验代码与SetYear类似:

func (d *Date) SetYear(year int) error { //增加一个error类型的返回值
	if year < 1 {
		return errors.New("invalid year") //若year是非法的,返回错误
	}
	d.Year = year
	return nil
}

func (d *Date) SetMonth(month int) error {
	if month < 1 || month > 12 {
		return errors.New("invalid month")
	}
	d.Month = month
	return nil
}

func (d *Date) SetDay(day int) error {
	if day < 1 || day > 31 {
		return errors.New("invalid day")
	}
	d.Day = day
	return nil
}

func main() {
        //try
}

在SetMonth中,测试提供的月份值,若小于1或大于12,就返回错误,否则设置字段并返回nil。

在SetDay中,测试提供的日期小于1或大于31,返回错误,有效值设置字段并返回nil。

字段仍可以设置为无效值

使用setter方法,提供校验很不错,但是用户可以直接设置struct字段,仍然能设置无效值。

没有任何方法能阻止直接设置Date struct字段,若这么做,就会绕过setter方法中的校验代码。

需要一个方式来保护这些字段,让Date类型只能使用setter方法来更新字段,Go提供了一个方式:把Date类型移动到另一个包,并将数据字段设置为非导出的。未导出的变量、函数等在大多数情况下无法导出,即使之前的magazine包中的Subscriber struct类型被导出(字段首字母非大写),但是它的字段并未被导出,导致在magazine包之外无法访问。

将Date类型移动到另外的包中

package calendar

import "errors"

type Date struct {
	year  int
	month int
	day   int
}  //将字段名称修改为小写字母让其不可被导出

func (d *Date) SetYear(year int) error {
	if year < 1 {
		return errors.New("invalid year")
	}
	d.year = year //更新字段名称匹配声明
	return nil
}

func (d *Date) SetMonth(month int) error {
	if month < 1 || month > 12 {
		return errors.New("invalid month")
	}
	d.month = month //更新字段名称匹配声明
	return nil
}

func (d *Date) SetDay(day int) error {
	if day < 1 || day > 31 {
		return errors.New("invalid day")
	}
	d.day = day  //更新字段名称匹配声明
	return nil
}

修改main包:

package main

import (
        "fmt"
        "xxx/xxx/calendar"
)

func main() {
        date := calendar.Date{}
        date.year = 2023
        date.month = 14
        date.day = 50
        fmt.Println(date)

        date = calendar.Date{year: 0, month: 0, day: -2}
        fmt.Println(date)
}

Date的字段未导出,从main包中访问,会导致编译错误。

通过导出的方法访问未导出的字段

可以间接访问字段。未导出的变量、struct字段、函数、方法等仍然能被相同包的导出的函数或者方法访问。所以当main包中的代码在Date值上调用导出方法SetYear时,SetYear可以修改Date的year字段,即使year是未导出的。导出方法SetYear可以更新未导出的month字段等。

修改main包来使用setter方法,可以更新Date值的字段。

package main

import (
        "fmt"
        "xxx/xxx/calendar"
        "log"
)

func main() {
        date := calendar.Date{}
        err := date.SetYear(2023)
        if err != nil {
                log.Fatal(err)
        }
        err = date.SetMonth(5)
        if err != nil {
                log.Fatal(err)
        }
        err = date.SetDay(27)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(date)
}

输出:

{2023 5 27}

未导出的变量、struct字段、函数、方法等仍然能够被相同包的导出的函数或方法访问。

此时若传入无效值,执行时会得到错误。

现在的Date值的字段只能通过setter方法更新,程序被保护,避免意外输出无效数据。

getter方法

将值设置给struct字段或者变量的方法称为 setter 方法。

获取struct字段或者变量的值的方法称为 getter 方法。

按照惯例,getter方法的名称应该与访问的字段或者变量的名字相同。(若希望方法被导出,首字母应该大写。)所以Date需要一个Year方法来访问year字段,Month方法来访问month字段,Day方法来访问day字段。

getter方法不需要修改接收器,直接使用Date的值作为接收器。若类型的任何方法接受接收器指针类型,为了一致性,通常所有的方法都应该这样做。由于必须对所有的setter方法使用接收器指针,也应对所有的getter方法使用指针。

package calendar

import "errors"

type Date struct {
	year  int
	month int
	day   int
}

func (d *Date) Year() int {  //与setter方法一致,使用一个接收器指针类型;Year与字段名称相同,为了导出首字母大写。
	return d.year   //返回字段值
}

func (d *Date) Month() int {
	return d.month
}

func (d *Date) Day() int {
	return d.day
}

修改main:

func main() {
        date := calendar.Date{}
        err := date.SetYear(2023)
        if err != nil {
                log.Fatal(err)
        }
        err = date.SetMonth(5)
        if err != nil {
                log.Fatal(err)
        }
        err = date.SetDay(27)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(date.Year())
        fmt.Println(date.Month())
        fmt.Println(date.Day())
}

输出(getter方法返回的值):

2023
5
27

封装

将程序中的数据隐藏在一部分代码中而对另一部分不可见的方法称为封装

封装可以用来防止无效数据,同样也可以修改程序代码的封装部分,而不用担心其他代码的访问,因为它们不可直接访问。

许多其他编程语言用类进行封装,类与Go的类型概念类似,但不完全相同,在Go中使用未导出的变量、struct字段、函数或者方法,把数据封装在包中。

Go通常在需要的时候才使用封装,不需要封装一个字段,通常导出并且允许直接访问。


问:其他语言不允许在定义封装的值的类之外访问该值,Go允许包内其他代码访问未导出代码是否安全?
答:通常一个包内的所有代码是一个开发者(或一组开发者)开发,包里所有的代码通常有相似的目的,相同包的代码的作者有可能需要访问未导出数据,并且有可能只用合理的方式来使用数据,所以在包内共享未导出数据是安全的。包外的代码有可能是其他开发者编写,但未导出代码是隐藏的,不会意外将其值改为无效数据。

问:其他语言所有getter方法的名字都以“Get”开头,比如GetName、GetCity等,在go中是否可以这样做?
答:Go允许这样做,但不推荐。Go社区大会讨论后决定在getter方法前面去掉Get前缀,保留它只会让后继者感到困惑。Go仍然对setter方法使用Set前缀,因为需要区分同一个字段的setter方法和getter方法。


练习:

要求:
1.将字段改为未导出的;
2.为每个字段增加一个getter方法(遵守约定:一个getter方法应该与它访问的字段名称相同,若需要导出,首字母大写);
3.为setter方法增加校验,SetLatitude若接收到小于-90或大于90的值返回错误,Setlongitude若接收到小于-180大于180的值返回错误。

package geo

import "errors"

type Coordinates struct {
	latitude  float64
	longitude float64 //字段需要未导出的
}

func (c *Coordinates) Latitude() float64 { //getter方法名应该与字段名相同,但是首字母需要大写;返回与字段类型相同
	return c.latitude
}

func (c *Coordinates) Longitude() float64 {
	return c.longitude
}

func (c *Coordinates) SetLatitude(latitude float64) error { //需要返回错误类型
	if latitude < -90 || latitude > 90 {
		return errors.New("invalid latitude") //返回一个新的错误值
	}
	c.latitude = latitude
	return nil //如果没有错误返回nil
}

func (c *Coordinates) SetLongitude(longitude float64) error {
	if longitude < -180 || longitude > 180 {
		return errors.New("invalid longitude")
	}
	c.longitude = longitude
	return nil
}

main 包代码要求:
1.遍历每个settter方法,保存error返回值
2.若error不为nil,使用log.Fatal函数记录错误并退出
3.若设置字段没有错误,调用所有getter方法输出字段值

package main

import (
	"fmt"
	"geo"
	"log"
)

func main() {
	coordinates := geo.Coordinates{}
	err := coordinates.SetLatitude(37.42) //保存返回的错误值
	if err != nil {                       //若有错误,记录并返回
		log.Fatal(err)
	}
	err = coordinates.SetLongitude(-1122.08) //一个无效值
	if err != nil {                          //若有错误记录并返回
		log.Fatal(err)
	}
	fmt.Println(coordinates.Latitude())
	fmt.Println(coordinates.Longitude()) //getter方法
}

在Event类型中嵌入Date类型

Date类型很棒,setter方法确保了只有合法的数据才能设置字段,getter方法让我们能取回这些值,现在只需给事件分配标题。

定义一个Event类型,并内嵌一个Date作为匿名字段。

package calendar

type Event struct {
        Title string
        Date   //使用一个匿名字段嵌入Date
}

在calendar包中创建另外一个文件,名为event.go(将把它放到已存在的date.go),在那个文件中,定义一个含有两个字段的类型:一个string类型的Title字段和一个匿名的Date字段。

未导出的字段不会被提升

将Date嵌入到Event类型中并不会导致Date的字段被提升到Event,Date字段是未导出的,并且Go不会将未导出的字段提升到封闭类型。确认字段被封装,就只能够被setter 方法和getter方法访问,不希望封装被字段提升绕开。

在main包中,若尝试通过包裹它的Event设置Date中的month字段,会得到错误:

package main

import "xxx/xxx/calendar"

func main() {
        event := calendar.Event{}
        event.month = 5  //未导出的Date字段并没有提升到Event
}
func main() {
        event := calendar.Event{}
        event.Date.year = 2023
}

使用点运算符链来返回Date字段并且直接访问其中的字段也无法工作,不能单独访问一个Date的未导出字段,当Date是Event的一部分的时,也不能访问Event中Date未导出的字段。

导出的方法像字段一样被提升

若嵌入了一个具有导出方法的struct类型,它的方法会被提升到外部类型,意味着可以调用这个方法,就像在外部类型上定义了该方法一样。

有一个定义了两种类型的包,MyType是一个struct类型,其中嵌入了第二个类型EmbeddedType,是一个匿名字段:

package mypackage  //类型在它们自己的包中

import "fmt"

type MyType struct {  //声明MyType是一个struct类型
        EmbeddedType  //EmbeddedType是一个嵌入MyType的类型
}

type EmbeddedType string  //声明一个被嵌入的类型(并不在意它是否是个struct)

func (e EmbeddedType) ExportedMethod() {  //这个方法会被提升至MyType
        fmt.Println("Hi from ExportedMethod on EmbeddedType")
}

func (e EmbeddedType) unexportedMethod() {  //这个方法不会被提升

}

因为EmbeddedType定义了一个导出的方法(名为ExportedMethod),这个类型被提升到MyType,并可以被MyType值所调用。

package main

import "mypackage"

func main() {
        value := mypackage.MyType{}
        value.ExportedMethod()  //调用从EmbeddedType提升的方法
}

对于未导出的字段,未导出的方法没有被提升,若尝试调用,会得到一个错误。

Date字段不会被提升到Event类型,因为它们未导出,但是Date上的getter和setter方法被导出了,它们提升到了Event类型。

意味着我们可以创建一个Event值,并在Event值上直接调用Date的getter和setter方法:

package main

import (
        "fmt"
        "xxx/xxx/calendar"
        "log"
)

func main() {
        event := calendar.Event{}
        err := event.SetYear(2023)
        if err != nil {
                log.Faltal(err)
        }
        err = event.SetMonth(5)
        if err != nil {
                log.Faltal(err)
        }
        err = event.SetDay(27)
        if err != nil {
                log.Faltal(err)
        }
        fmt.Println(event.Year())
        fmt.Println(event.Month())
        fmt.Println(event.Day())  //这个Date的getter方法被提升到了Event
}

当然,也可以使用运算符链来调用Date值上的方式:

fmt.Println(event.Date.Year())
fmt.Println(event.Date.Month())
fmt.Println(event.Date.Day())   //获取Event的Date字段,然后调用其getter方法

封装Event的Title字段

由于Event struct的Title字段被导出,仍然可以直接访问它。

这会面临与之前Date字段相同的问题,例如,Title字符串没有长度限制:

func main() {
        event := calendar.Event{}
        event.Title = "An extremely long title that is impractical to print"
        fmt.Println(event.Title)
}

将Title字段封装起来,这样就能校验新的值,将其命名为title,然后增加setter和getter方法,来自 unicode/utf8 包中的RuneCountInString函数被用来确保string中没有过多的字符。

package calendar

import (
        "errors"
        "unicode/utf8"
)

type Event struct {
        title string  //修改为非导出
        Date
}

func (e *Event) Title() string {   //getter方法
        return e.title
}

func (e *Event) SetTitle(title string) error {  //setter方法
	if utf.RuneCountInString(title) > 30 {
                return errors.New("invalid title")
	}
        e.title = title
        return nil
}

提升的方法与外部类型的方法共存

现在已经为title字段添加了getter和setter方法,若title长度超过30个字符,会返回错误。

Event类型的Title和SetTitle方法与从嵌入的Date类型提升的方法一同存在,导入calendar包的代码可以让所有的方法被视为属于Event类型,而不用考虑它们真正定义在哪个类型上。

func main() {
        event := calendar.Event{}
        err := event.SetTitle("Mom's birthday")  //定义在Event上
        if err != nil {
                log.Fatal(err)
        }
        err = event.SetYear(2023)  //从Date提升
        if err != nil {
                log.Fatal(err)
        }
        err = event.SetMonth(5)  //从Date提升
        if err != nil {
                log.Fatal(err)
        }
        err = event.SetDay(27)  //从Date提升
        if err != nil {
                log.Fatal(err)
        }
        fmt.Println(event.Title())  //Event自己定义的
        fmt.Println(event.Year())   //从Date提升
        fmt.Println(event.Month())  //从Date提升
        fmt.Println(event.Day())    //从Date提升
}

现在能在Event上直接调用Title和SetTitle方法,并调用方法设置年月日就像它们是属于Event一样,它们实际上是在Date上定义的,但我们不需要关心。

方法提升允许使用其他类型的方法就像使用自己的一样,可以用这个来组合类型,该类型组合了多种其他类型的方法,这可以帮助保持代码整洁,且不牺牲便利性。

Golang打怪升级

11.接口

9.定义类型

comments powered by Disqus