结构体
1. 概念
Go通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起,然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,可以通过new函数来创建。
组成结构体类型的那些数据成为字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。
2.结构体定义
结构体定义的一般方式如下:
type identifier struct {field1 type1field2 type2...}
type T struce {a, b int}也是合法的语法,它更适用于简单的结构体;
结构体里的字段都有名字,像field1、field2等,如果字段在代码中从来也不会被用到,那么可以命名它为_ 。
结构体的字段可以是任何类型,任何是结构体本身,也可以是函数或者接口:
// 自定义int类型type Counter int// 自定义map[string]string类型type User map[string]string// 自定义函数类型type Callback func(...string)// 定义接收自定义类型(函数类型)为参数的函数func printResult(pf Callback, list ...string) {pf(list)}func column(list ...string) {for _, e := range list {fmt.Println(e)}fmt.Println()}
可以声明结构体类型的一个变量,然后给它的字段赋值:
var s Ts.a = 5s.b = 8
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T),如果需要可以把这条语句放在不同的行:
var t *Tt = new(T)
常用方法是: t := new(T),变量t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值;
声明var t T也会给t分配内存,并零值化内存,但这个时候t是类型T。在这两种方式中,t通常被称做类型T的一个实例(instance)或对象(object);
示例,structs_fields.go:
package mainimport "fmt"type struct1 struct {i1 intf1 float32str string}func main() {ms := new(struct1)ms.i1 = 10ms.f1 = 15.5ms.str = "hi"fmt.Printf("The int is: %d\n", ms.i1)fmt.Printf("The float is: %f\n", ms.f1)fmt.Printf("The string is: %s\n", ms.str)fmt.Println(ms)}
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value
同样,使用点号符可以获取结构体字段的值: strctname.fieldname
无论变量是一个结构体类型还是一个结构体类型指针,都是用同样的选择器符来引用结构体的字段:
type myStruct struct {i int}var v myStruct // v是结构体类型变量var p * myStruct // p是指向一个结构体类型变量的指针v.ip.i
3.结构体初始化
ms := &struct1{10, 15.5, "Chris"} // 此时ms的类型为*struct1
或者:
var ms struct1ms = struct1{10, 15.5, "Chris"}
混合字面量语法(composite literal syntax) &struct1{a, b, c}是一种简写,底层仍然会调用new(),这里值的顺序必须按照字段顺序来写。
时间间隔是使用结构体的一个典型例子:
type Interval struct {start intend int}
初始化方式:
intr := Interval{0, 3}intr := Interval{end:5, start:1}intr := Interval{end:5}
第一行,值必须以字段在结构体定义时的顺序给出,&不是必须的;第二行,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像第三行中那样;
下面例子显示了一个结构体Person,一个方法,方法有一个类型为*Person的参数(因此对象本身是可以被改变的),以及三种调用这方法的不同方式:
package mainimport ("fmt""strings")type Person struct {firstName stringlastName string}func upPerson(p *Person) {p.firstName = strings.ToUpper(p.firstName)p.lastName = strings.ToUpper(p.lastName)}func main() {// 1-struct as a value typevar pers1 Personpers1.firstName = "Chris"pers1.lastName = "Woodward"upPerson(&pers1)fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)// 2-struct as a pointerpers2 := new(Person)pers2.firstName = "Chris"pers2.lastName = "Woodward"// (*pers2).lastName = "Woodward" // 这是合法的upPerson(pers2)fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)// 3-struct as a literalpers3 := &Person{"Chris", "Woodward"}upPerson(pers3)fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)}
结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。
结构体转换,Go中的类型转换遵循严格的规则。当为结构体定义了一个alias类型时,此结构体类型和它的alias类型都有相同的底层类型,它们可以如示例那样互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
package mainimport "fmt"type number struct {f float32}type nr number // alias typefunc main() {a := number{5.0}b := nr{5.0}// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32// var c number = b // compile-error: cannot use b (type nr) as type number in assignment// needs a conversion:var c = number(b)fmt.Println(a, b, c)}
4.匿名结构体
在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置
package mainimport "fmt"func main() {var coordinate struct {longitude float64latitude float64}me := struct {name stringage intaddr string}{"silence", 30, "xian"}studs := []struct {name stringaddr string}{{"silence", "xian"}, {"woniu", "beijing"}}fmt.Println(coordinate, coordinate.longitude, coordinate.latitude)fmt.Println(me, me.name, me.age, me.addr)fmt.Println(studs)for i, stud := range studs {fmt.Println(i, stud, stud.name, stud.addr)}}
5.命名嵌入
结构体命名嵌入是指结构体中的属性对应的类型也是结构体
- 定义
type Address struct {region stringstreet stringno string}type User struct {name stringtel stringaddr Address}
- 声明和初始化
var u1 Userfmt.Printf("%T, %#v\n", u1, u1)var a1 Address = Address{"xian","xian road","001",}var u2 User = User{"zky", "123", a1}u3 := User{name: "zky1",tel: "123456",addr: Address{"beijing","haidian","002",},}fmt.Println(u1, u2, u3)
- 属性的访问和修改
fmt.Println(u2.addr)fmt.Println(u2.addr.no)u2.addr.no = "002"fmt.Println(u2)
6.匿名嵌入
结构体匿名嵌入是指将已定义的结构体名直接声明在新的结构体中,从而实现对以后已有类型的扩展和修改
- 定义
type Employee struct{Usersalary float64title string}
- 声明&初始化
// 使用已有变量初始化ekk01 := Employee{u2, 1000, "dev"}ekk02 := Employee{User{"zky","132132",Address{"xian","jingye","002",},},2000,"ops",}// 使用指定名称初始化ekk03 := Employee{User: User{"zky2","178178",Address{"beijing","jingye","003",},},salary: 1000,title: "jiagou",}fmt.Println(ekk01)fmt.Println(ekk02)fmt.Println(ekk03)
在初始化匿名嵌入的结构体对象时需要遵循树状声明的结构,对于匿名嵌入的结构体可以使用结构体名来指定初始化参数
- 属性访问和修改
// 针对匿名嵌入结构体属性,可以通过对象.结构体名称.属性名访问和修改属性值fmt.Println(ekk03.User.name, ekk03.User.tel, ekk03.User.addr, ekk03.salary, ekk03.title)ekk03.User.tel = "9876"fmt.Println(ekk03)// 针对匿名嵌入结构体属性值访问和修改时可省略结构体名称fmt.Println(ekk03.name, ekk03.tel, ekk03.addr, ekk03.salary, ekk03.title)ekk03.tel = "654321"fmt.Println(ekk03)
在访问和修改嵌入结构体的属性值时,可以通过对象名.结构体名称.属性名的方式进行访问和修改,结构体名称可以省略(匿名成员有一个隐式的名称),因为不能嵌套两个相同名称的结构体。当被嵌入结构体和嵌入结构体有相同的属性名时,在访问和修改嵌入结构体成员的属性值时不能省略结构体名称。
7.指针类型嵌入
结构体嵌入(命名&匿名)类型也可以为结构体指针
- 定义
// 命名嵌入结构体指针type PUser struct {name stringtel stringaddr *Address}// 匿名嵌入结构体指针type PEmployee struct {*PUsersalary float64title string}
- 声明&初始化&操作
package mainimport "fmt"type Address struct {region stringstreet stringno string}// 命名嵌入结构体指针type PUser struct {name stringtel stringaddr *Address}// 匿名嵌入结构体指针type PEmployee struct {*PUsersalary float64title string}func main() {// 声明和初始化嵌入指针结构体对象paddr := Address{"xian", "jingye", "001"}puser := PUser{"zky", "123123", &paddr}pemployee := PEmployee{PUser: &puser,salary: 1000,title: "jiagoushi",}fmt.Printf("%#v\n", pemployee)//属性访问和修改fmt.Println(paddr)fmt.Println(puser.addr)puser.addr.no = "002"fmt.Println(paddr)fmt.Println(puser.addr)fmt.Println(puser.name)fmt.Println(pemployee.PUser.name)fmt.Println(pemployee.name)pemployee.name = "silence"fmt.Println(puser.name)fmt.Println(pemployee.PUser.name)fmt.Println(pemployee.name)}
使用属性为指针类型底层共享数据结构,当底层数据发生变化,所有引用都会发生影响
package mainimport "fmt"type Employee struct {Usersalary float64title string}type Address struct {region stringstreet stringno string}type User struct {name stringtel stringaddr Address}func main() {// 声明和初始化嵌入值结构体对象vaddr := Address{"xian", "jinyelu", "001"}vuser := User{"zky", "123123", vaddr}vemployee := Employee{User: vuser,salary: 12223,title: "jiagou",}fmt.Printf("%#v\n", vemployee)// 属性访问和修改fmt.Println(vaddr) // no:001fmt.Println(vuser.addr) // no:001vuser.addr.no = "002"fmt.Println(vaddr) // no:001fmt.Println(vuser.addr) // no:002fmt.Println(vuser.name) // zkyfmt.Println(vemployee.User.name) // zkyfmt.Println(vemployee.name) // zkyvemployee.name = "silence"fmt.Println(vuser.name) // zkyfmt.Println(vemployee.User.name) // silencefmt.Println(vemployee.name) // silence}
使用属性为值类型,则在复制时发生拷贝,两者不相互影响
8.可见性
结构体首字母大写则包外可见(公开的),否则仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否则仅包内可访问(内部的)
组合:
- 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名;
- 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名;
- 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能在包内使用;
- 结构体名首字母小写,属性名小写:结构体只能在包内使用;
方法
方法是为特定类型定义的,只能由该类型调用的函数。
在 Go 中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是int、bool、string或数组的别名类型。但是接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type…
最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。
一个类型加上它的方法等价于面向对象中的一个类。
一个重要的区别是:在Go中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件,唯一的要求是:它们必须是同一个包;
类型T(或 _T)上的所有方法的集合叫做类型T(或 _T)的方法集(method set);
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在2个或多个不同的接收者类型上存储,比如在同一个包里这么做是允许的:
func (a *denseMatrix) Add(b Matrix) Matrixfunc (a *sparseMatrix) Add(b Matrix) Matrix
别名类型没有原始类型上已经定义过的方法。
1.定义
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) {...}
在方法名之前,func 关键字之后的括号中指定receiver
如果 recv 是receiver的实例,Method1是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()
如果 recv 是一个指针,Go会自动解引用;
如果方法不需要使用recv的值,可以用_替换它,比如:
func (_ receiver_type) methodName(parameter_list) (return_value_list) {...}
recv就像是面向对象语言中的 this 或 self,但是Go中并没有这两个关键字。
下面是一个结构体上的简单方法的例子:
package mainimport "fmt"type TwoInts struct {a intb int}func (tn *TwoInts) AddThem() int {return tn.a + tn.b}func (tn *TwoInts) AddToParam(param int) int {return tn.a + tn.b + param}func main() {two1 := new(TwoInts)two1.a = 12two1.b = 10fmt.Printf("The sum is: %d\n", two1.AddThem())fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))two2 := TwoInts{3, 4}fmt.Printf("The sum is: %d\n", two2.AddThem())}
下面是非结构体类型上方法的例子:
package mainimport "fmt"type IntVector []intfunc (v IntVector) Sum() (s int) {for _, x := range v {s += x}return}func main() {fmt.Println(IntVector{1, 2, 3}.Sum())}
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在int、float或类似这些的类型上定义。试图在int类型上定义方法会得到一个编译错误:
cannot define new methods on non-local type int
比如想在 time.Time 上定义如下方法:
func (t time.Time) first3Chars() string {return time.LocalTime().String()[0:3]}
类型在其他的,或是非本地的包里定义,在它上面定义方法都会得到和上面同样的错误;
但是有一个间接的方式:可以先定义该类型(比如:int或float)的别名类型,然后再为别名类型定义方法。或者像下面这样讲它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
package mainimport ("fmt""time")type myTime struct {time.Time // anonymous field}func (t myTime) first3Chars() string {return t.Time.String()[0:3]}func main() {m:= myTime{time.Now()}// 调用匿名Time上的String方法fmt.Println("Full time now:", m.String())// 调用myTime.first3Charsfmt.Println("First 3 chars", m.first3Chars())}
2.函数和方法的区别
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
不要忘记Method1后边的括号(),否则会引发编译器错误:method recv.Method1 is not expression, must be called
接收者必须有一个显示的名字,这个名字必须在方法中被使用。
receiver_type叫做(接收者)基本类型,这个类型必须在和方法同样的包中被声明。
在Go中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
3.指针或值作为接收者
鉴于性能的原因,recv最常见的是一个指向receiver_type的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在receiver类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
下面的例子pointer_value.go作了说明:change()接受一个指向B的指针,并改变它内部的成员;write()通过拷贝接受B的值并只输出B的内容。注意Go为我们做了探测工作,我们自己并没有指出指针上调用方法,Go替我们做了这些事情。b1是值而b2是指针,方法都支持运行了。
package mainimport "fmt"type B struct {thing int}func (b *B) change() {b.thing = 1}func (b B) write() string {return fmt.Sprint(b)}func main() {var b1 B // b1是值b1.change()fmt.Println(b1.write())b2:= new(B) // b2是指针b2.change()fmt.Println(b2.write())}
在值和指针上调用方法:可以有连接到类型的方法,也可以有连接到类型指针的方法;
但是这没关系:对于类型T,如果在*T上存在方法Meth(),并且t是这个类型的变量,那么t.Meth()会被自动转换为(&t).Meth()
指针方法和值方法都可以在指针或非指针上被调用。
4.方法和未导出字段
考虑person2.go中的person包:类型Person被明确的导出了,但是它的字段没有被导出。例如在use_person2.go中p.firstName就是错误的。该如何在另一个程序中修改或者只是读取一个 Person的名字呢?
这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。
person2.go
package persontype Person struct {firstName stringlastName string}func (p *Person) FirstName() string {return p.firstName}func (p *Person) SetFristName(newName string) {p.firstName = newName}
use_person2.go
package mainimport ("fmt""moduledemo/person")func main() {p := new(person.Person)p.SetFristName("Eric")fmt.Println(p.FirstName())}
5.内嵌类型的方法和继承
当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。
下面示例:假定有一个Engine接口类型,一个Car结构体类型,它包含一个Engine类型的匿名字段:
type Engine interface {Start()Stop()}type Car struct {Engine}func (c *Car) GoToWorkIn() {c.Start()c.Stop()}
下面是 method3.go 的完整例子,它展示了内嵌结构体上的方法可以直接在外层类型的实例上调用:
package mainimport ("fmt""math")type Point struct {x, y float64}func (p *Point) Abs() float64 {return math.Sqrt(p.x*p.x + p.y*p.y)}type NamePoint struct {Pointname string}func main() {n := &NamePoint{Point{3, 4}, "Pythagoras"}fmt.Println(n.Abs())}
内嵌将一个已存在类型的字段和方法注入到另一个类型里:匿名字段上的方法晋升成为了外层类型的方法。当时类型可以有只作用于本身实例而不作用于内嵌父类型上的方法。
可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型的对应的方法;
结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法;
6.如何在类型中嵌入功能
A:聚合(或组合),包含一个所需功能类型的具名字段;
B:内嵌,内嵌(匿名地)所需功能类型;
为了使这些概念具体化,假设有一个Customer类型,我们想让它通过Log类型来包含日志功能,Log类型只是简单地包含一个累积的消息(当时它可以是复杂的)。如果想让特定类型都具备日志功能,你可以实现一个这样的Log类型,然后将它作为特定类型的一个字段,并提供Log(),它返回这个日志的引用。
方式A可通过如下方法实现:
package mainimport "fmt"type Log struct {msg string}type Customer struct {Name stringlog *Log}func main() {c := new(Customer)c.Name = "Barak"c.log = new(Log)c.log.msg = "1 - can!"c = &Customer{"Barak", &Log{"1 - can!"}}c.Log().Add("2 - place")fmt.Println(c.Log())}func (l *Log) Add(s string) {l.msg += "\n" + s}func (l *Log) String() string {return l.msg}func (c *Customer) Log() *Log {return c.log}
相对的方式B像这样:
package mainimport "fmt"type Log struct {msg string}type Customer struct {Name stringLog}func main() {c := &Customer{"Barak", Log{"1 - can!"}}c.Add("2 - place")fmt.Println(c)}func (l *Log) Add(s string) {l.msg += "\n" + s}func (l *Log) String() string {return l.msg}func (c *Customer) String() string {return c.Name + "\nLog:" + fmt.Sprintln(c.Log.String())}
内嵌的类型不需要指针,Customer也不需要Add方法,它使用Log的Add方法,Customer有自己的 String方法,并且在它里面调用了Log的String方法;
如果内嵌类型嵌入了其他类型,也是可以的,那些类型的方法可以直接在外层类型中使用;
因此一个好的策略是创建一些小的、可服用的类型作为一个工具箱,用于组成域类型;
7.多重继承
多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(Python例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在Go中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承;
例子,有一个类型CameraPhone,通过它可以Call(),属于类型Phone的方法,也可以TakeAPicture(),属于类型Camera的方法:
package mainimport "fmt"type Camera struct{}func (c *Camera) TakeAPicture() string {return "Click"}type Phone struct{}func (p *Phone) Call() string {return "Ring"}type CameraPhone struct {CameraPhone}func main() {cp := new(CameraPhone)fmt.Println("new CameraPhone")fmt.Println("It a Camera:", cp.TakeAPicture())fmt.Println("It a Phone too:", cp.Call())}
8.通用方法和方法命名
在编程中一些基本操作会一遍又一遍的出现,比如打开(Open)、关闭(Close)、读(Read)等,并且它们都有一个大致的意思:打开(Open)可以作用于一个文件、一个网络连接、一个数据库连接等。具体的实现可能千差万别,但是基本的概念是一致的。在Go中,通过使用接口,标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如Open()、Read()、Write()等。想写规范的Go程序,就应该遵守这些约定,给方法合适的名字和签名,就像哪些通用方法那样。这样做会使Go开发的软件更加具有一致性和可读性。比如:如果需要一个convert-to-string方法,应该命名为String(),而不是ToString()。
9.类型的String()方法和格式化描述符
当定义了一个有很多方法的类型时,十之八九会使用String()方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。如果类型定义了String()方法,它会被用在fmt.Printf()中生成默认的数据:等同于使用格式化描述符%v产生的数据。还有print()和println()也会自动使用。
method_string.go:
package mainimport ("fmt""strconv")type TwoInts struct {a intb int}func main() {two1 := new(TwoInts)two1.a = 12two1.b = 10fmt.Printf("two1 is: %v\n", two1)fmt.Println("two1 is:", two1)fmt.Printf("two1 is: %T\n", two1)fmt.Printf("two1 is: %#v\n", two1)}func (tn *TwoInts) String() string {return "(" + strconv.Itoa(tn.a) + "/" + strconv.Itoa(tn.b) + ")"}
当广泛使用一个自定义类型时,最好为它定义String()方法。从上面的例子也可以看到,格式化描述符%T会给出类型的完全规格,%#v会给出实例的完整输出,包括它的字段。
不要在String()方法里面调用涉及String()方法的方法。
10.垃圾回收和SetFinalizer
在Go运行时中有一个独立的进程,即垃圾收集器(GC),会搜索并释放程序中不再使用的变量和结构占用的内存。可以通过runtime包来访问GC进程。
通过调用runtime.GC()函数可以显示的触发GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用runtime.GC(),它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为GC进程在执行)。
如果想知道当前的内存状态,可以使用:
var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("%d Kb\n", m.Alloc/1024)
如果需要在一个对象obj被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现:
runtime.SetFinalizer(obj, func(obj *typeObj))
func(obj *typeObj)需要一个typeObj类型的指针参数obj,特殊操作会在它上面执行。func也可以是一个匿名函数。
在对象被GC进程选中并从内存中移除以前,SetFinalizer都不会执行,即使程序正常结束或者发生错误。
