Go语言学习笔记(八)
一、创建方法和接口
在(七)中介绍了结构体,它是一种创建数据结构的方式,使用点表示法来访问结构体中的数据。当涉及到更复杂的操作时,结构体就有些心有余而力不足了。为此,Go提供了另一种操作数据的方式——通过方法来操作
本篇中首先介绍方法以及如何创建和使用与特定数据类型相关联的方法集,再介绍一种描述方法集的方式接口。
1 使用方法
方法类似函数,但有一点不同:在关键词func后面添加了另一个参数部分,用于接受单个参数
type Movie struct{
Name string
Rating float32
}
func (m *Movie)summary()string{
//code
}
上面这个例子中为结构体Movie添加了一个方法。
- 在方法声明中关键词func后面多了一个参数——接收者。严格的说,方法接受者是一种类型,这里是指向结构体Movie的指针。
- 接下来是方法名、参数以及返回类型。除多了包含接收者的参数部分外,方法与第4章介绍的函数完全相同。可将接收者视为与方法相关联的东西。
- 方法的作用,通过声明方法summary,让结构体Movie的任何实例都可以使用它。
- 个人觉得方法像是类外函数,调用某一个自定义的类对象或者是对类对象进行某种操作
为什么有了函数还要有方法??
例如下面的函数与前面的方法声明等价。
type Movie struct{
Name string
Rating float64
}
func summary(m *Movie)string{
//code
}
函数summary和结构体Movie相互依赖,但他们之间没有直接关系。例如,如果不能访问结构体Movie的定义,就无法声明函数summary。
如果使用函数则在每个使用函数或结构体的地方,都需要包含函数和结构体的定义,这会导致代码重复。
另外,函数发生任何改变,都必须随之修改多个地方。这样看来在函数与结构体关系密切时,使用方法更合理。
方法summary的实现将float64等级制转换为字符串并设置其格式。使用方法的优点在于:只需编写方法实现一次就可以对结构体的任何实例进行调用。
package main
import (
"fmt"
"strconv"
)
type Movie struct {
Name string
Rating float64
}
func (m *Movie) summary() string {
r := strconv.FormatFloat(m.Rating, 'f', 1, 64)
return m.Name + ", " + r
}
func main() {
m := Movie{
Name: "fadsf",
Rating: 3.2,
}
fmt.Println(m.summary())//输出fadsf, 3.2
}
这里的FormatFloat是一个实现指定类型浮点数输出的函数,
第一个参数是对应的要转换的float64或者float32位的数据,
第二个参数是要转换的浮点数类型(‘f’,‘F’,‘e’,‘E’,‘g’,‘G’),
第三个参数是指的输出浮点数的精度,如果是<0,就返回最少的位数(小数点以后第一位不为0的数位)来表示该数,如果是>0的数则返回对应位数的值,
最后一个参数,表示浮点数的存储结构,因为在Go中float分为32位和64位,因此需要传入32或者64。
2 创建方法集
- 方法集是可对特定数据类型进行调用的一组方法。在Go语言中,任何数据类型都可有相关联的方法集,这让您能够在数据类型和方法之间建立关系,如前面的结构体Movie示例所示。
- 方法集可包含的方法数量不受限制,这是一种封装功能和创建库代码的有效方式
- 处理球体时,假设要计算其表面及和体积。在种情况下,非常适合使用结构体和方法集。
- 在方法中可以访问结构体的Radius值,这是使用点表示法访问的。
- 我觉得方法集和函数的唯一不同就是前者更方便一些
package main
import (
"fmt"
"math"
)
type Sphere struct {
Radius float64
}
func (s *Sphere) SurfaceArea() float64 {
return float64(4) * math.Pi * (s.Radius * s.Radius)
}
func (s *Sphere) Volume() float64 {
radiusCubed := s.Radius * s.Radius * s.Radius
return (float64(4) / float64(3)) * math.Pi * radiusCubed
}
func main() {
s := Sphere{
Radius: 5,
}
fmt.Println(s.SurfaceArea())//输出314.1592653589793
fmt.Println(s.Volume())//输出523.5987755982989
}
3 使用方法和指针
方法的接受者可以是指针,也可以是值,两者差别非常微妙
当接收者是指针时
package main
import "fmt"
type Triangle struct {
base float64
height float64
}
func (t *Triangle) area() float64 {
return 0.5 * (t.base * t.height)
}
func main() {
t := Triangle{base: 3, height: 1}
fmt.Println(t.area())//输出1.5
}
之前我们就了解到了,指针指向的是一块内存空间,对指针的操作会反映到相应的内存空间上去,由此我们不难想出,向方法传递值引用和指针引用的区别:当在函数中对结构体进行修改时,值引用不会修改原对象,指针引用会修改原对象,因为值引用,实际上是在方法内部创建了一个原对象的副本,只有指针引用是切实的作用在原对象的内存空间中的。例子如下:
//值引用
package main
import "fmt"
type Triangle struct {
base float64
height float64
}
func (t Triangle) changeBase(f float64) {
t.base = f
return
}
func main() {
t := Triangle{base: 3, height: 1}
t.changeBase(4)
fmt.Println(t.base)/输出3
}
- 我们发现在方法内部对值引用参数进行修改时,原对象并没有发生改变
//指针引用
package main
import "fmt"
type Triangle struct {
base float64
height float64
}
func (t *Triangle) changeBase(f float64) {
t.base = f
return
}
func main() {
t := Triangle{base: 3, height: 1}
t.changeBase(4)
fmt.Println(t.base)//输出4
}
结果显而易见,不是么?
4 使用接口
- 在Go语言中,接口指定了一个方法集,这是实现模块化的强大方式。我们可以将接口视为方法集的蓝本,他描述了
方法集中的所用方法,但没有实现它们。接口功能强大,因为它充当了方法集规范,这意味着可在符合借口要求的前提下随便实现更换- 接口描述了方法集中的所有方法,并制定了每个方法的函数签名
- 要满足接口的要求,只要实现了它指定的方法集,且函数签名正确无误即可
- 我是这样理解接口的,接口是一些方法的声明组成的集合,只要方法的函数签名和返回类型和接口中的某个方法定义相吻合,我们就可以说这个方法属于这个接口中的方法集,同时,方法的接受者不会影响方法的归属类,这意味着,不同的结构体可以使用相同名称的方法,有点像函数的重载
实例如下
package main
import (
"errors"
"fmt"
)
//定义了一个名为Robot的接口
//接口中的方法集只有一个PowerOn()
type Robot interface {
PowerOn() error
}
//定义结构体及其对应的方法
type T850 struct {
Name string
}
func (a *T850) PowerOn() error {
return nil
}
//再定义不同的结构体和方法
type R2D2 struct {
Broken bool
}
func (r *R2D2) PowerOn() error {
if r.Broken {
return errors.New("R2D2 is broken")
} else {
return nil
}
}
//定义了一个调用函数
func Boot(r Robot) error {
return r.PowerOn()
}
func main() {
t := T850{
Name: "fadfasf",
}
r := R2D2{
Broken: true,
}
err := Boot(&r)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Robot is powered on!")
}
err = Boot(&t)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Robot is powered on!")
}
}
- 在这里我发现定义的调用接口的函数
Boot的参数是Robot,但是在调用Boot的时候,传递的参数却是我们定义的两个结构体的指针,同时他们也是接口方法集中方法的接收者- 在这里可以这样理解
interface类型,并不是我们实际意义上认知的那种类型,所有类型的变量(有接口中的方法集定义),都可以转换成这种类型- 所以,是不是可以将接口理解为一类实体(方法的接收者)的别称
5 一些问题
a.函数和方法有何不同?
严格来说,方法和函数的唯一差别在于,方法多了一个指定接收者的参数,这让我们能够针对数据类型调用方法调用方法,从而提高了代码重用性和模块化程度
b.在什么情况下使用指针引用,什么情况下使用值引用?
如果需要修改原始结构体中的数据,就使用指针;如果要操作原始数据的副本,就私用值引用
c.接口的实现可包含接口中没有的方法么?
可以,可在接口的实现中添加额外的方法,但这仅适用于结构体,而不适用于接口。
6 关于接口的后续思考
问了度娘之后,发现这么一个实例
package main
import (
"fmt"
)
//定义interface
type VowelsFinder interface {
FindVowels() []rune
}
type MyString string
//实现接口
func (ms MyString) FindVowels() []rune {
var vowels []rune
for _, rune := range ms {
if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
vowels = append(vowels, rune)
}
}
return vowels
}
func main() {
name := MyString("Sam Anderson") // 类型转换
var v VowelsFinder // 定义一个接口类型的变量
v = name
fmt.Printf("Vowels are %c", v.FindVowels())
}
在这个例子的最后两行,我们定义的MyString类型的变量直接被赋值给了
interface v,这说明了接口的使用方法,用生活中的例子解释,不同级别的员工那不同级别的薪资,我们的接口就是一个计算薪资的机器,不同的人使用会有不同的计算方法和结果。
7 接口的其他说明
7.1 空接口
具有0个方法的接口称为空接口。它表示为interface {}。由于空接口有0个方法,所有类型都实现了空接口。
package main
import (
"fmt"
)
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
func main() {
// 任何类型的变量传入都可以
s := "Hello World"
i := 55
strt := struct {
name string
}{
name: "Naveen R",
}
describe(s)
describe(i)
describe(strt)
}
输出结果如下:
Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}
7.2 类型断言
类型断言用于提取接口的基础值,语法:i.(T)
package main
import(
"fmt"
)
func assert(i interface{}){
s:= i.(int)
fmt.Println(s)
}
func main(){
var s interface{} = 55.0
assert(s)
}
如上,我们选择非int类型的interface{}进行传参,结果如下:
panic: interface conversion: interface {} is float64, not int
我们还可以进行修改程序
package main
import (
"fmt"
)
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)//输出:56 true
var i interface{} = "Steven Paul"
assert(i)//输出:0 false
}
在这里,我们使用ok捕获了assert函数中的断言结果,如果不进行捕捉,就会报上面的panic,这里涉及到了Go语言中的panic机制。我们可以这样理解,panic相当于一个程序崩了,然后Go相当于操作系统,崩溃的程序不能一直在电脑内存中保存,它捕捉到了这个崩溃的程序(自动执行的),然后我们使用定义了捕捉崩溃程序的笼子(ok),这是手动执行的。
7.3 类型判断
我觉得,类型判断相当于多个类型断言的结合。
类型判断的语法类似于类型断言。在类型断言的语法i.(type)中,类型type应该由类型转换的关键字type替换。让我们看看它如何在下面的程序中起作用。
package main
import (
"fmt"
)
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("String: %s\n", i.(string))
case int:
fmt.Printf("Int: %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen")
findType(77)
findType(89.98)
/*
String: Naveen
Int: 77
Unknown type
*/
}
将类型与接口进行比较
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
p := Person{
name: "Naveen R",
age: 25,
}
findType(p)
}
一个需要思考的程序
package main
import "fmt"
type Describer interface {
Describe()
}
type St string
func (s St) Describe() {
fmt.Println("被调用le!")
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
case string:
fmt.Println("String 变量")
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
st := St("我的字符串")
findType(p)
}
/*
String 变量
被调用le!
*/
参考书籍
[1]: 【Go语言入门经典】[英] 乔治·奥尔波 著 张海燕 译
题外话:
- 关于这部分接口的内容,我是没怎么看懂,他有些像是C++的函数重载,又像是一个方法族,一个方法百种使用,只要接收者不同,一个方法可以被n多的结构体使用,感觉有点晕乎乎的,而且在书中的关于Boot函数的定义,参数类型时接口,传入的参数却是接收者,emm迷
- 所以我还是去问了度娘,果然,还是没咋看懂。
Go语言中的方法、接口与类型操作解析

949

被折叠的 条评论
为什么被折叠?



