面向对象
面向对象
Go 并不是一个纯面向对象的编程语言。Go 没有提供类 class,但提供了结构体 struct 和方法 method,可以在结构体上添加方法,从而实现捆绑数据和行为,达到与类相似的效果。
| 概念 | Go 的实现 | 传统 OOP(如 Java) |
|---|---|---|
| 类 | 结构体 struct | class |
| 对象 | 结构体实例 | 类实例(对象) |
| 方法 | 带接收者的函数 | 类成员方法 |
| 继承 | 组合(Composition)/ 匿名字段 | extends 继承 |
| 多态 | 接口隐式实现 | 接口显式实现 / implements |
| 构造函数 | New() 工厂函数 | 构造函数 |
结构体
什么是结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
结构体的定义和初始化
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}一旦定义了结构体类型,它就能用于变量的声明:
variable_name := structure_variable_type {value1, value2...valuen}初始化结构体的三种方式:
// 1. 按照顺序提供初始化值
P := person{"Tom", 25}
// 2. 通过 field:value 的方式初始化,这样可以任意顺序
P := person{age:24, name:"Tom"}
// 3. new 方式,未设置初始值的,会赋予类型的默认初始值
p := new(person)
p.age = 24结构体的访问
通过点 . 操作符访问结构的各个字段。
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
fmt.Printf("Book 1 title : %s\n", Book1.title)
fmt.Printf("Book 1 author : %s\n", Book1.author)
fmt.Printf("Book 1 subject : %s\n", Book1.subject)
fmt.Printf("Book 1 book_id : %d\n", Book1.book_id)
/* 打印 Book2 信息 */
fmt.Printf("Book 2 title : %s\n", Book2.title)
fmt.Printf("Book 2 author : %s\n", Book2.author)
fmt.Printf("Book 2 subject : %s\n", Book2.subject)
fmt.Printf("Book 2 book_id : %d\n", Book2.book_id)
}运行结果:
Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : www.runoob.com
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700结构体指针
可以创建指向结构体的指针:
var struct_pointer *Books以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前:
struct_pointer = &Book1;使用结构体指针访问结构体成员,直接使用 . 操作符(Go 会自动解引用):
struct_pointer.title;package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* Declare Book1 of type Book */
var Book2 Books /* Declare Book2 of type Book */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
printBook(&Book1)
/* 打印 Book2 信息 */
printBook(&Book2)
}
func printBook(book *Books) {
fmt.Printf("Book title : %s\n", book.title)
fmt.Printf("Book author : %s\n", book.author)
fmt.Printf("Book subject : %s\n", book.subject)
fmt.Printf("Book book_id : %d\n", book.book_id)
}结构体实例化也可以通过 String() 方法自定义输出格式:
package main
import "fmt"
type Books struct {
}
func (s Books) String() string {
return "data"
}
func main() {
fmt.Printf("%v\n", Books{})
}结构体的匿名字段
可以用字段来创建结构,这些字段只包含一个没有字段名的类型。这些字段被称为匿名字段。
在类型中,使用不写字段名的方式,使用另一个类型。实际就是字段的继承。
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认 Student 就包含了 Human 的所有字段
speciality string
}
func main() {
// 我们初始化一个学生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 我们访问相应的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}可以使用
.的方式进行调用匿名字段中的属性值其中可以将匿名字段理解为字段名和字段类型都是同一个
基于上面的理解,所以可以
mark.Human = Human{"Marcus", 55, 220}和mark.Human.age -= 1若存在匿名字段中的字段与非匿名字段名字相同,则最外层的优先访问,就近原则
通过匿名访问和修改字段相当有用,不仅仅是 struct 字段,所有的内置类型和自定义类型都可以作为匿名字段。
结构体嵌套
一个结构体可能包含一个字段,而这个字段反过来就是一个结构体。这些结构被称为嵌套结构。
package main
import (
"fmt"
)
type Address struct {
city, state string
}
type Person struct {
name string
age int
address Address
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.address.city)
fmt.Println("State:", p.address.state)
}提升字段
在结构体中属于匿名结构体的字段称为提升字段,因为它们可以被访问,就好像它们属于拥有匿名结构体字段的结构一样。
package main
import (
"fmt"
)
type Address struct {
city, state string
}
type Person struct {
name string
age int
Address // 匿名结构体
}
func main() {
var p Person
p.name = "Naveen"
p.age = 50
p.Address = Address{
city: "Chicago",
state: "Illinois",
}
fmt.Println("Name:", p.name)
fmt.Println("Age:", p.age)
fmt.Println("City:", p.city) // city is promoted field
fmt.Println("State:", p.state) // state is promoted field
}运行结果:
Name: Naveen
Age: 50
City: Chicago
State: Illinois导出结构体和字段
如果结构体类型以大写字母开头,那么它是一个导出类型,可以从其他包访问它。类似地,如果结构体的字段以大写开头,则可以从其他包访问它们。
示例代码:
- 在
computer目录下,创建文件spec.go:
package computer
type Spec struct { // exported struct
Maker string // exported field
model string // unexported field
Price int // exported field
}- 创建
main.go文件:
package main
import "structs/computer"
import "fmt"
func main() {
var spec computer.Spec
spec.Maker = "apple"
spec.Price = 50000
fmt.Println("Spec:", spec)
}目录结构如下:
src structs computer spec.go main.go
结构体比较
结构体是值类型,如果每个字段具有可比性,则是可比较的。如果它们对应的字段相等,则认为两个结构体变量是相等的。
package main
import (
"fmt"
)
type name struct {
firstName string
lastName string
}
func main() {
name1 := name{"Steve", "Jobs"}
name2 := name{"Steve", "Jobs"}
if name1 == name2 {
fmt.Println("name1 and name2 are equal")
} else {
fmt.Println("name1 and name2 are not equal")
}
name3 := name{firstName: "Steve", lastName: "Jobs"}
name4 := name{}
name4.firstName = "Steve"
if name3 == name4 {
fmt.Println("name3 and name4 are equal")
} else {
fmt.Println("name3 and name4 are not equal")
}
}运行结果:
name1 and name2 are equal
name3 and name4 are not equal如果结构变量包含的字段是不可比较的,那么结构变量是不可比较的
package main
import (
"fmt"
)
type image struct {
data map[int]int
}
func main() {
image1 := image{data: map[int]int{
0: 155,
}}
image2 := image{data: map[int]int{
0: 155,
}}
if image1 == image2 { // 编译错误:无法比较
fmt.Println("image1 and image2 are equal")
}
}结构体作为函数的参数
结构体可以作为函数参数使用:
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* book 2 描述 */
Book2.title = "Python 教程"
Book2.author = "www.runoob.com"
Book2.subject = "Python 语言教程"
Book2.book_id = 6495700
/* 打印 Book1 信息 */
printBook(Book1)
/* 打印 Book2 信息 */
printBook(Book2)
}
func printBook(book Books) {
fmt.Printf("Book title : %s\n", book.title)
fmt.Printf("Book author : %s\n", book.author)
fmt.Printf("Book subject : %s\n", book.subject)
fmt.Printf("Book book_id : %d\n", book.book_id)
}make 与 new
make 用于内建类型(map、slice 和 channel)的内存分配。new 用于各种类型的内存分配。
内建函数 new 本质上说跟其它语言中的同名函数功能一样:new(T) 分配了零值填充的 T 类型的内存空间,并且返回其地址,即一个 *T 类型的值。有一点非常重要:new 返回指针。
内建函数 make(T, args) 与 new(T) 有着不同的功能,make 只能创建 slice、map 和 channel,并且返回一个有初始值(非零)的 T 类型,而不是 *T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个 slice,是一个包含指向数据(内部 array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice 为 nil。对于 slice、map 和 channel 来说,make 初始化了内部的数据结构,填充适当的值。
make 返回初始化后的(非零)值。
| 内建函数 | 适用类型 | 返回值 | 说明 |
|---|---|---|---|
new(T) | 任意类型 | *T(指针) | 分配零值内存,返回指针 |
make(T, args) | slice / map / channel | T(初始化的值) | 初始化内部数据结构,返回非零值 |
方法
什么是方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接收者的函数,接收者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
方法只是一个函数,它带有一个特殊的接收器类型,是在 func 关键字和方法名之间编写的。接收器可以是 struct 类型或非 struct 类型。接收方可以在方法内部访问。
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。
在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
方法的语法
定义方法的语法:
func (t Type) methodName(parameter list) (return list) {
}
func funcName(parameter list) (return list) {
}示例代码:
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() // Calling displaySalary() method of Employee type
}可以定义相同的方法名——只要接收者类型不同即可:
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width * r.height
}
// 该 method 属于 Circle 类型对象中的方法
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}运行结果:
Area of r1 is: 24
Area of r2 is: 36
Area of c1 is: 314.1592653589793
Area of c2 is: 1963.4954084936207- 虽然 method 的名字一模一样,但是如果接收者不一样,那么 method 就不一样
- method 里面可以访问接收者的字段
- 调用 method 通过
.访问,就像 struct 里面访问字段一样
方法 vs 函数
既然我们已经有了函数,为什么还要使用方法?
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
displaySalary(emp1)
}在上面的程序中,
displaySalary方法被转换为一个函数,而Employeestruct 作为参数传递给它。这个程序也产生了相同的输出:Salary of Sam Adolf is $5000.。
| 对比维度 | 方法 | 函数 |
|---|---|---|
| 定义语法 | func (receiver Type) name(params) returns | func name(params) returns |
| 所属 | 属于某个类型 | 独立存在 |
| 相同名称 | 不同接收者类型可重名 | 同一包内不可重名 |
| 调用方式 | instance.method() | funcName(args) |
| 设计意图 | 模拟类的行为 | 通用过程式逻辑 |
使用方法的几个原因:
- Go 不是一种纯粹面向对象的编程语言,它不支持类。因此,类型的方法是一种实现类似于类的行为的方式。
- 相同名称的方法可以在不同的类型上定义,而具有相同名称的函数是不允许的。
变量作用域
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
局部变量: 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量: 在函数体外声明的变量称之为全局变量,首字母大写全局变量可以在整个包甚至外部包(被导出后)使用。
package main
import "fmt"
/* 声明全局变量 */
var g int
func main() {
/* 声明局部变量 */
var a, b int
/* 初始化参数 */
a = 10
b = 20
g = a + b
fmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}运行结果:
结果: a = 10, b = 20 and g = 30形式参数: 形式参数会作为函数的局部变量来使用。
指针接收者 vs 值接收者
若不是以指针作为接收者,实际只是获取了一个 copy,而不能真正改变接收者中的数据:
func (b *Box) SetColor(c Color) {
b.color = c
}示例代码:
package main
import (
"fmt"
)
type Rectangle struct {
width, height int
}
func (r *Rectangle) setVal() {
r.height = 20
}
func main() {
p := Rectangle{1, 2}
s := p
p.setVal()
fmt.Println(p.height, s.height)
}运行结果:
20 2如果没有那个 *,则值就是 2 2。
| 接收者类型 | 是否修改原值 | 适用场景 |
|---|---|---|
值接收者 (t Type) | 否(操作副本) | 只读操作、小对象 |
指针接收者 (t *Type) | 是(操作原值) | 需要修改接收者、大对象避免拷贝 |
method 继承
method 是可以继承的,如果匿名字段实现了一个 method,那么包含这个匿名字段的 struct 也能调用该 method:
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human // 匿名字段
company string
}
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}运行结果:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam you can call me on 111-888-XXXXmethod 重写
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // 匿名字段
school string
}
type Employee struct {
Human // 匿名字段
company string
}
// Human 定义 method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Employee 的 method 重写 Human 的 method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) // Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}运行结果:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam, I work at Golang Inc. Call me on 111-888-XXXX- 方法是可以继承和重写的
- 存在继承关系时,按照就近原则进行调用
接口
什么是接口
面向对象世界中的接口的一般定义是"接口定义对象的行为"。它表示让指定对象应该做什么。实现这种行为的方法(实现细节)是针对对象的。
在 Go 中,接口是一组方法签名。当类型为接口中的所有方法提供定义时,它被称为实现接口。它与 OOP 非常相似。接口指定了类型应该具有的方法,类型决定了如何实现这些方法。
它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了该接口。
接口的定义语法
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}示例代码:
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}运行结果:
I am Nokia, I can call you!
I am iPhone, I can call you!- interface 可以被任意的对象实现
- 一个对象可以实现任意多个 interface
- 任意的类型都实现了空 interface(我们这样定义:
interface{}),也就是包含 0 个 method 的 interface
接口值
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // 匿名字段
school string
loan float32
}
type Employee struct {
Human // 匿名字段
company string
money float32
}
// Human 实现 SayHi 方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human 实现 Sing 方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
// Employee 重写 Human 的 SayHi 方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone)
}
// Interface Men 被 Human、Student 和 Employee 实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}
// 定义 Men 类型的变量 i
var i Men
// i 能存储 Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
// i 也能存储 Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
// 定义了 slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
// 这三个都是不同类型的元素,但是他们实现了 interface 同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}运行结果:
This is Mike, a Student:
Hi, I am Mike you can call me on 222-222-XXX
La la la la... November rain
This is Tom, an Employee:
Hi, I am Sam, I work at Things Ltd.. Call me on 444-222-XXX
La la la la... Born to be wild
Let's use a slice of Men and see what happens
Hi, I am Paul you can call me on 111-222-XXX
Hi, I am Sam, I work at Golang Inc.. Call me on 444-222-XXX
Hi, I am Mike you can call me on 222-222-XXX那么 interface 里面到底能存什么值呢?如果我们定义了一个 interface 的变量,那么这个变量里面可以存实现这个 interface 的任意类型的对象。例如上面例子中,我们定义了一个 Men interface 类型的变量 m,那么 m 里面可以存 Human、Student 或者 Employee 值。
当然,使用指针的方式,也是可以的
但是,接口对象不能调用实现对象的属性
interface 作为函数参数: interface 的变量可以持有任意实现该 interface 类型的对象,这给我们编写函数(包括 method)提供了一些额外的思考,我们是不是可以通过定义 interface 参数,让函数接受各种类型的参数。
接口的嵌入
package main
import "fmt"
type Human interface {
Len()
}
type Student interface {
Human
}
type Test struct {
}
func (h *Test) Len() {
fmt.Println("成功")
}
func main() {
var s Student
s = new(Test)
s.Len()
}一个通过嵌入实现接口组合的示例:
package test
import (
"fmt"
)
type Controller struct {
M int32
}
type Something interface {
Get()
Post()
}
func (c *Controller) Get() {
fmt.Print("GET")
}
func (c *Controller) Post() {
fmt.Print("POST")
}package main
import (
"fmt"
"test"
)
type T struct {
test.Controller
}
func (t *T) Get() {
// new(test.Controller).Get()
fmt.Print("T")
}
func (t *T) Post() {
fmt.Print("T")
}
func main() {
var something test.Something
something = new(T)
var t T
t.M = 1
// t.Controller.M = 1
something.Get()
}Controller 实现了所有的 Something 接口方法,当结构体 T 中嵌入 Controller 结构体的时候,T 就相当于 Java 中的继承,T 继承了 Controller,因此,T 可以不用重写所有的 Something 接口中的方法,因为父构造器已经实现了接口。
如果 Controller 没有实现 Something 接口方法,则 T 要调用 Something 中方法,就要实现其所有方法。
如果 something = new(test.Controller) 则调用的是 Controller 中的 Get 方法。
T 可以使用 Controller 结构体中定义的变量。
鸭子类型与隐式实现
鸭子类型:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。
Duck Typing(鸭子类型)是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,通过接口的方式完美支持鸭子类型。
而在静态语言如 Java、C++ 中,必须要显示地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。
| 对比维度 | Go(隐式实现) | Java(显式实现) |
|---|---|---|
| 实现声明 | 不需显式声明,实现所有方法即可 | 需要 implements 关键字 |
| 解耦程度 | 高,类型和接口独立演化 | 低,类型创建时就要绑定接口 |
| 编译检查 | 使用接口变量时检查 | 声明时就检查 |
| 灵活度 | 后期可随时实现接口 | 必须预先设计 |
| 设计理念 | 鸭子类型 | 契约式设计 |
Go 语言作为一门现代静态语言,有后发优势。它引入了动态语言的便利,同时又会进行静态语言的类型检查。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。
总结:鸭子类型是一种动态语言的风格,Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。
多态性
Go 中的多态性是在接口的帮助下实现的。正如我们已经讨论过的,接口可以在 Go 中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则实现一个接口。
任何定义接口所有方法的类型都被称为隐式地实现该接口。
类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现 Go 中的多态性。
举个例子,一个虚构的组织有两种项目的收入:固定的账单和时间和材料。组织的净收入是由这些项目的收入之和计算出来的。
首先我们定义一个接口 Income:
type Income interface {
calculate() int
source() string
}接下来,定义两个结构体:FixedBilling 和 TimeAndMaterial:
type FixedBilling struct {
projectName string
biddedAmount int
}type TimeAndMaterial struct {
projectName string
noOfHours int
hourlyRate int
}下一步是定义这些结构体类型的方法,计算并返回实际收入和收入来源:
func (fb FixedBilling) calculate() int {
return fb.biddedAmount
}
func (fb FixedBilling) source() string {
return fb.projectName
}
func (tm TimeAndMaterial) calculate() int {
return tm.noOfHours * tm.hourlyRate
}
func (tm TimeAndMaterial) source() string {
return tm.projectName
}接下来,我们来声明一下计算和打印总收入的 calculateNetIncome 函数:
func calculateNetIncome(ic []Income) {
var netincome int = 0
for _, income := range ic {
fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
netincome += income.calculate()
}
fmt.Printf("Net income of organisation = $%d", netincome)
}上面的 calculateNetIncome 函数接受一部分 Income 接口作为参数。它通过遍历切片和调用 calculate() 方法来计算总收入。它还通过调用 source() 方法来显示收入来源。根据收入接口的具体类型,将调用不同的 calculate() 和 source() 方法。因此,我们在 calculateNetIncome 函数中实现了多态。
在未来,如果组织增加了一种新的收入来源,这个函数仍然可以正确地计算总收入,而没有一行代码更改。
最后我们写以下主函数:
func main() {
project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
incomeStreams := []Income{project1, project2, project3}
calculateNetIncome(incomeStreams)
}运行结果:
Income From Project 1 = $5000
Income From Project 2 = $10000
Income From Project 3 = $4000
Net income of organisation = $19000假设该组织通过广告找到了新的收入来源。让我们看看如何简单地添加新的收入方式和计算总收入,而不用对 calculateNetIncome 函数做任何更改。由于多态性,这样是可行的。
首先让我们定义 Advertisement 类型和 calculate() 和 source() 方法:
type Advertisement struct {
adName string
CPC int
noOfClicks int
}
func (a Advertisement) calculate() int {
return a.CPC * a.noOfClicks
}
func (a Advertisement) source() string {
return a.adName
}广告类型有三个字段 adName、CPC(cost per click)和 noOfClicks(点击数)。广告的总收入是 CPC 和 noOfClicks 的乘积。
修改主函数:
func main() {
project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
bannerAd := Advertisement{adName: "Banner Ad", CPC: 2, noOfClicks: 500}
popupAd := Advertisement{adName: "Popup Ad", CPC: 5, noOfClicks: 750}
incomeStreams := []Income{project1, project2, project3, bannerAd, popupAd}
calculateNetIncome(incomeStreams)
}运行结果:
Income From Project 1 = $5000
Income From Project 2 = $10000
Income From Project 3 = $4000
Income From Banner Ad = $1000
Income From Popup Ad = $3750
Net income of organisation = $23750综上,我们没有对 calculateNetIncome 函数做任何更改,尽管我们添加了新的收入方式。它只是因为多态性而起作用。由于新的 Advertisement 类型也实现了 Income 接口,我们可以将它添加到 incomeStreams 切片中。calculateNetIncome 函数也在没有任何更改的情况下工作,因为它可以调用 Advertisement 类型的 calculate() 和 source() 方法。
接口断言
前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。
语法格式:
// 安全类型断言
<目标类型的值>, <布尔参数> := <表达式>.( 目标类型 )
// 非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )示例代码:
package main
import "fmt"
func main() {
var i1 interface{} = new(Student)
s := i1.(Student) // 不安全,如果断言失败,会直接 panic
fmt.Println(s)
var i2 interface{} = new(Student)
s, ok := i2.(Student) // 安全,断言失败也不会 panic,只是 ok 的值为 false
if ok {
fmt.Println(s)
}
}
type Student struct {
}断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。
switch ins := s.(type) {
case Triangle:
fmt.Println("三角形。。。", ins.a, ins.b, ins.c)
case Circle:
fmt.Println("圆形。。。。", ins.radius)
case int:
fmt.Println("整型数据。。")
}总结: 接口对象不能调用接口实现对象的属性。
type 关键字
type 是 Go 语法里的重要而且常用的关键字,type 绝不只是对应于 C/C++ 中的 typedef。搞清楚 type 的使用,就容易理解 Go 语言中的核心概念 struct、interface、函数等的使用。
类型定义
定义结构体:
// 定义结构体
type person struct {
name string // 注意后面不能有逗号
age int
}定义接口:
type USB interface {
start()
end()
}定义其他的新类型:
语法:
type 类型名 Type示例代码:
package main
import "fmt"
type myint int
type mystr string
func main() {
var i1 myint
var i2 = 100
i1 = 100
fmt.Println(i1)
// i1 = i2 // cannot use i2 (type int) as type myint in assignment
fmt.Println(i1, i2)
var name mystr
name = "王二狗"
var s1 string
s1 = "李小花"
fmt.Println(name)
fmt.Println(s1)
name = s1 // cannot use s1 (type string) as type mystr in assignment
}定义函数的类型:
Go 语言支持函数式编程,可以使用高阶编程语法。一个函数可以作为另一个函数的参数,也可以作为另一个函数的返回值,那么在定义这个高阶函数的时候,如果函数的类型比较复杂,我们可以使用 type 来定义这个函数的类型:
package main
import (
"fmt"
"strconv"
)
func main() {
res1 := fun1()
fmt.Println(res1(10, 20))
}
type my_fun func(int, int) (string)
// fun1() 函数的返回值是 my_func 类型
func fun1() my_fun {
fun := func(a, b int) string {
s := strconv.Itoa(a) + strconv.Itoa(b)
return s
}
return fun
}类型别名
类型别名的写法为:
type 别名 = Type类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
类型别名是 Go 1.9 版本添加的新功能。主要用于代码升级、迁移中类型的兼容性问题。在 C/C++ 语言中,代码重构升级可以使用宏快速定义新的一段代码。Go 语言中没有选择加入宏,而是将解决重构中最麻烦的类型名变更问题。
在 Go 1.9 版本之前的内建类型定义的代码是这样写的:
type byte uint8
type rune int32而在 Go 1.9 版本之后变为:
type byte = uint8
type rune = int32这个修改就是配合类型别名而进行的修改。
示例代码:
package main
import (
"fmt"
)
func main() {
var i1 myint
var i2 = 100
i1 = 100
fmt.Println(i1)
// i1 = i2 // cannot use i2 (type int) as type myint in assignment
fmt.Println(i1, i2)
var i3 myint2
i3 = i2
fmt.Println(i1, i2, i3)
}
type myint int // 定义新类型
type myint2 = int // 不是重新定义类型,只是给 int 起别名| 写法 | 含义 | 是否需要类型转换 |
|---|---|---|
type myint int | 基于 int 创建新类型 | 是,myint 和 int 是不同的类型 |
type myint = int | 给 int 创建别名 | 否,本质是同一个类型 |
New() 函数替代构造函数
Go 不支持构造函数。如果某个类型的零值不可用,则程序员的任务是不导出该类型以防止其他包的访问,并提供一个名为 NewT(parameters) 的函数,该函数初始化类型 T 和所需的值。在 Go 中,它是一个命名一个函数的约定,它创建了一个 T 类型的值给 NewT(parameters)。这就像一个构造函数。如果包只定义了一个类型,那么它的一个约定就是将这个函数命名为 New(parameters) 而不是 NewT(parameters)。
首先在 src 目录下创建一个 package 命名为 oop,在 oop 目录下,再创建一个子目录命名为 employee,在该目录下创建一个 go 文件命名为 employee.go。
目录结构:oop -> employee -> employee.go
在 employee.go 文件中保存以下代码:
package employee
import (
"fmt"
)
type Employee struct {
FirstName string
LastName string
TotalLeaves int
LeavesTaken int
}
func (e Employee) LeavesRemaining() {
fmt.Printf("%s %s has %d leaves remaining", e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
}然后在 oop 目录下,创建文件并命名为 main.go:
package main
import "oop/employee"
func main() {
e := employee.Employee{
FirstName: "Sam",
LastName: "Adolf",
TotalLeaves: 30,
LeavesTaken: 20,
}
e.LeavesRemaining()
}运行结果:
Sam Adolf has 10 leaves remaining我们上面写的程序看起来不错,但是里面有一个微妙的问题。让我们看看当我们用 0 值定义 employee struct 时会发生什么。更改 main 的内容:
package main
import "oop/employee"
func main() {
var e employee.Employee
e.LeavesRemaining()
}运行结果:
has 0 leaves remaining通过运行结果可以知道,使用 Employee 的零值创建的变量是不可用的。它没有有效的名、姓,也没有有效的保留细节。在其他的 OOP 语言中,比如 Java,这个问题可以通过使用构造函数来解决。使用参数化构造函数可以创建一个有效的对象。
更改 employee.go 的代码:
首先修改 employee 结构体为非导出,并创建一个函数 New(),它将创建一个新 Employee:
package employee
import (
"fmt"
)
type employee struct {
firstName string
lastName string
totalLeaves int
leavesTaken int
}
func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {
e := employee{firstName, lastName, totalLeave, leavesTaken}
return e
}
func (e employee) LeavesRemaining() {
fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}我们在这里做了一些重要的改变。我们已经将 Employee struct 的起始字母设置为小写,即我们已经将类型 Employee struct 更改为 employee struct。通过这样做,我们成功地未导出了 employee 结构并阻止了其他包的访问。将未导出的结构的所有字段都导出为未导出的方法是很好的做法,除非有特定的需要导出它们。由于我们不需要在包之外的任何地方使用 employee struct 的字段,所以我们也没有导出所有字段。
由于 employee 是未导出的,所以不可能从其他包中创建类型 employee 的值。因此,我们提供了一个导出的 New 函数。将所需的参数作为输入并返回新创建的 employee。
修改 main.go 代码:
package main
import "oop/employee"
func main() {
e := employee.New("Sam", "Adolf", 30, 20)
e.LeavesRemaining()
}运行结果:
Sam Adolf has 10 leaves remaining因此,我们可以明白,虽然 Go 不支持类,但是结构体可以有效地使用,在使用构造函数的位置,使用 New(parameters) 的方法即可。
组合(Composition)替代继承
Go 不支持继承,但它支持组合。组合的一般定义是"放在一起"。构图的一个例子就是汽车。汽车是由轮子、发动机和其他各种部件组成的。
博客文章就是一个完美的组合例子。每个博客都有标题、内容和作者信息。这可以用组合完美地表示出来。
通过嵌入结构体实现组合
可以通过将一个 struct 类型嵌入到另一个结构中实现。
package main
import (
"fmt"
)
/*
我们创建了一个 author struct,它包含字段名、lastName 和 bio。
我们还添加了一个方法 fullName(),将作者作为接收者类型,这将返回作者的全名。
*/
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
/*
post struct 有字段标题、内容。它还有一个嵌入式匿名字段 author。
这个字段表示 post struct 是由 author 组成的。
现在 post struct 可以访问作者结构的所有字段和方法。
我们还在 post struct 中添加了 details() 方法,它打印出作者的标题、内容、全名和 bio。
*/
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.author.fullName())
fmt.Println("Bio: ", p.author.bio)
}
func main() {
author1 := author{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1 := post{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1,
}
post1.details()
}运行结果:
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast嵌入结构体的切片: 不能匿名嵌入切片,需要一个字段名。
type website struct {
posts []post
}
func (w website) contents() {
fmt.Println("Contents of Website\n")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}完整示例代码:
package main
import (
"fmt"
)
type author struct {
firstName string
lastName string
bio string
}
func (a author) fullName() string {
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
type post struct {
title string
content string
author
}
func (p post) details() {
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.fullName())
fmt.Println("Bio: ", p.bio)
}
type website struct {
posts []post
}
func (w website) contents() {
fmt.Println("Contents of Website\n")
for _, v := range w.posts {
v.details()
fmt.Println()
}
}
func main() {
author1 := author{
"Naveen",
"Ramanathan",
"Golang Enthusiast",
}
post1 := post{
"Inheritance in Go",
"Go supports composition instead of inheritance",
author1,
}
post2 := post{
"Struct instead of Classes in Go",
"Go does not support classes but methods can be added to structs",
author1,
}
post3 := post{
"Concurrency",
"Go is a concurrent language and not a parallel one",
author1,
}
w := website{
posts: []post{post1, post2, post3},
}
w.contents()
}运行结果:
Contents of Website
Title: Inheritance in Go
Content: Go supports composition instead of inheritance
Author: Naveen Ramanathan
Bio: Golang Enthusiast
Title: Struct instead of Classes in Go
Content: Go does not support classes but methods can be added to structs
Author: Naveen Ramanathan
Bio: Golang Enthusiast
Title: Concurrency
Content: Go is a concurrent language and not a parallel one
Author: Naveen Ramanathan
Bio: Golang Enthusiast非本地类型不能定义方法
能够随意地为各种类型起名字,是否意味着可以在自己包里为这些类型任意添加方法?
package main
import (
"time"
)
// 定义 time.Duration 的别名为 MyDuration
type MyDuration = time.Duration
// 为 MyDuration 添加一个函数
func (m MyDuration) EasySet(a string) { // cannot define new methods on non-local type time.Duration
}
func main() {
}以上代码报错。报错信息:cannot define new methods on non-local type time.Duration
编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法。非本地方法指的就是使用 time.Duration 的代码所在的包,也就是 main 包。因为 time.Duration 是在 time 包中定义的,在 main 包中使用。time.Duration 包与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。
解决这个问题有下面两种方法:
- 将类型别名改为类型定义:
type MyDuration time.Duration,也就是将MyDuration从别名改为类型。 - 将
MyDuration的别名定义放在time包中。
结构体成员嵌入时使用别名
当类型别名作为结构体嵌入的成员时会发生什么情况?
package main
import (
"fmt"
)
type Person struct {
name string
}
func (p Person) Show() {
fmt.Println("Person-->", p.name)
}
// 类型别名
type People = Person
type Student struct {
// 嵌入两个结构
Person
People
}
func (p People) Show2() {
fmt.Println("People------>", p.name)
}
func main() {
//
var s Student
// s.name = "王二狗" // ambiguous selector s.name
s.People.name = "李小花"
s.Person.name = "王二狗"
// s.Show() // ambiguous selector s.Show
s.Person.Show()
s.People.Show2()
fmt.Printf("%T,%T\n", s.Person, s.People) // main.Person,main.Person
}在通过 s 直接访问 name 的时候,或者 s 直接调用 Show() 方法,因为两个类型都有 name 字段和 Show() 方法,会发生歧义,证明 People 的本质确实是 Person 类型。