Go语言基础(四)

一.结构体

1.什么是结构体

结构体是用户定义的类型,表示若干个字段(Field)的集合。有时应该把数据整合在一起,而不是让这些数据没有联系。这种情况下可以使用结构体。

例如,一个职员有 firstNamelastName 和 age 三个属性,而把这些属性组合在一个结构体 employee 中就很合理。

总的来说:结构体就是一系列属性的集合

2.结构体语法

语法

// type关键字 结构体名字 struct{}

一个基本的结构体,只包含属性

type Person struct {
    name string
    // 两个类型一样可以写一行
    age,sex int
}

3.结构体定义

定义时没有初始化查看他的空值

// 定义时没有初始化查看他的空值
var person Person
fmt.Println(person)  // 输出:{ 0 0},他的空值是结构体属性的空值

结构体的空值是他每个字段的空值

由此可得,结构体是值引用类型,修改结构体的属性不会影响原值

定义并初始化的时候有两种传参方式

// 定义并初始化的时候有两种传参方式,他们的不同
var person Person = Person{name:"sxc"}  // 关键字传参,可以传指定数量的
var person Person = Person{"sxc",18,1}  // 位置传参,所有的参数都必须传,并且传递位置固定
fmt.Println(person.name)

关键字传参可以传任意数量的,位置传参必须都传

4.匿名结构体

匿名结构体:在main函数内部定义,只使用一次,可以用来存储经常使用的变量

// 分别定义两个变量并使用他们
name := "sxc"
age := 18
fmt.Println(name,age)

当使用多次时,麻烦且不直观

//可以使用匿名结构体集成到一起,这样显示的更直观
student := struct {
    name string
    age int
}{}
student.name = "sxc"
student.age = 18
fmt.Println(student.name,student.age)

5.结构体指针

Go语言帮我们做好处理,使用指针也可以点出对应的字段

// 结构体的指针
var person *Person = &Person{"sxc",18,1}
fmt.Println(person)  // 输出&{sxc 18 1},直观的显示
fmt.Println((*person).name)  // 反解之后可以输出属性值
fmt.Println(person.name)  // go帮我们做好处理,可以直接使用指针.属性的方式输出

6.匿名字段

匿名字段,类型名当做字段名,当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段(Anonymous Field)。

// 匿名字段
type Test struct {
    string  // 类型名当做字段名
    int
}

匿名字段的使用

// 匿名字段,类型名当做字段名
var test111 Test = Test{"hello",15}  // 位置传参
var test123 Test = Test{string:"hello",int:15}  // 关键字就是类型名
fmt.Println(test111.string)
fmt.Println(test123.string)

7.结构体嵌套

结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。

type Person struct {
    name string
    // 两个类型一样可以写一行
    age,sex int
    //hobby Hobby  // 结构体嵌套
}
// 定义一个Hobby结构体,在Person结构体中使用
type Hobby struct {
    id int
    hobbyname string
}

定义和使用

// 结构体嵌套
var person Person = Person{name:"sxc",hobby:Hobby{id:5,hobbyname:"sing"}}  // 结构体嵌套的定义
fmt.Println(person.hobby.hobbyname)  // 结构体嵌套的使用

8.结构体嵌套+匿名字段(变量的提升)

type Person struct {
    name string
    // 两个类型一样可以写一行
    age,sex int
    Hobby  // 结构体嵌套+匿名字段
}
// 定义一个Hobby结构体,在Person结构体中使用
type Hobby struct {
    id int
    //name string  // 和上层结构体有同名字段
    hobbyname string
}

提升变量

// 结构体嵌套+匿名字段(用作变量提升)
var person Person = Person{name:"sxc",Hobby:Hobby{id:5,hobbyname:"sing"}}
fmt.Println(person.Hobby.hobbyname)
fmt.Println(person.hobbyname)  // 使用匿名字段后,我们可以在person这层就能直接点出Hobby的字段
fmt.Println(person.Hobby.name)  // 注意当两个结构体中有同名字段时,内部结构体的字段不会提升

类似于面向对象的继承,子类可以使用父类的属性

9.结构体相等性(Structs Equality)

结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。

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")
    }
}

在上面的代码中,结构体类型 name 包含两个 string 类型。由于字符串是可比较的,因此可以比较两个 name 类型的结构体变量。

上面代码中 name1 和 name2 相等,而 name3 和 name4 不相等。该程序会输出:

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")
    }
}

在上面代码中,结构体类型 image 包含一个 map 类型的字段。由于 map 类型是不可比较的,因此 image1 和 image2 也不可比较。如果运行该程序,编译器会报错:main.go:18: invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)。

二.方法

1.什么是方法

方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。

下面就是创建一个方法的语法。

func (t Type) methodName(parameter list) {
}

2.给结构体绑定方法

// 给Person结构体加一个打印名字的方法
func (p *Person)changeName(a string)  {
    (*p).name = a
    fmt.Println(p)
}

person结构体

type Person struct {
    name string
    age,sex int
    Hobby
}

3.为什么我们已经有函数了还需要方法呢?

  • Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
  • 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。

有了方法之后,我们就能使用结构体生成的对象点出方法来

// 可以用结构体生成的对象点出来使用
var p = Person{name:"sxc",Hobby:Hobby{hobbyname:"sing"}}
p.printName()

这样结构体中拥有自己的字段属性,并且我们给他增加了方法,这样这个结构体就类似于面向对象的类,可以通过生成的对象点出来属性和方法

4.值接收器和指针接收器,两者何时使用

我们定义一个改名的方法

// 给Person结构体加一个打印名字的方法
func (p *Person)changeName(a string)  {
    (*p).name = a
    fmt.Println(p)
}

使用值和指针接收器调用该方法

// 值和指针类型都可以调方法
(&p).changeName("zzj")  // 我们可以使用指针来调用该方法
p.changeName("zzj")  
fmt.Println(p.name)  
// 值调用时在方法内部改变了,但因为结构体是一个值类型,故没有真正的修改,指针调用时修改的是指针对应的内存地址,故真正的修改了

故:

当操作不需影响到结构体时使用值接收器

当操作需要影响到结构体时使用指针接收器

5.方法或函数,使用值或指针

a.在方法中使用值接收器

b.在函数中使用值参数

c.在方法中使用指针接收器 

d.在函数中使用指针参数

定义四个对应的方法或函数

// 方法中使用值接收器
func (p Person)printName1 ()  {
    fmt.Println(p.name)
}

// 方法中使用指针接收器
func (p *Person)printName2 ()  {
    fmt.Println(p.name)
}

// 函数中使用值参数
func printName3 (p Person)  {
    fmt.Println(p.name)
}

// 函数中使用指针参数
func printName4 (p *Person)  {
    fmt.Println(p.name)
}

各自调用上述方法或函数

//调用值接收器方法
p.printName1()
//调用指针接收器方法
p.printName2()

//调用值参数函数
printName3(p)
//调用指针参数函数
printName4(p)

得出结论:

从上面四个不同的方法或函数中可得,值或者指针接收器都能调值或者指针方法

而函数指定传什么参数就需要传什么参数

6.给非结构体定义方法

首先尝试给int类型定义方法

//给非结构体定义方法
func (a *int)printNum () {
    *a++
}

发现int等基本类型都不支持自定义方法

我们使用起别名的方法

type MyInt int  // 给int起别名

在给MyInt定义方法

func (a *MyInt) addNum () MyInt{  // 由于MyInt是值类型,故我们需要修改他的指针才能真正的修改值
    *a++
    return *a
}

调用查看结果

var a MyInt = MyInt(6)  // 初始化生成对象
fmt.Println(a.addNum())  // 调用add方法
fmt.Println(a)

两次打印的结果一致,故修改成功

三.接口

1.什么是接口

在面向对象的领域里,接口一般这样定义:接口定义一个对象的行为。接口只指定了对象应该做什么,至于如何实现这个行为(即实现细节),则由对象本身去确定。

在 Go 语言中,接口就是方法签名(Method Signature)的集合。当一个类型定义了接口中的所有方法,我们称它实现了该接口。这与面向对象编程(OOP)的说法很类似。接口指定了一个类型应该具有的方法,并由该类型决定如何实现这些方法。

简单来说:接口就是一系列方法的集合

2.接口的声明与实现

接口的语法

// 接口的语法
type 接口名 interface {
    方法一
    方法二
}

声明一个接口

// 定义一个鸭子接口
type Duck interface {
    run()
    speak()
}

接口的实现:只要在结构体中实现了接口中所有的方法就是实现了接口

首先定义一个结构体

// 定义高级鸭子结构体
type GDuck struct {
    name string
    age int
    wife bool
}

然后实现接口中的方法

// 实现接口
func (p PDuck)run(){
    fmt.Println("我是普通鸭子,我的名字叫",p.name)
}
func (p PDuck)speak(){
    fmt.Println("我是普通鸭子,我嘎嘎叫")
}

3.类型断言

我们需要不同的结构体实现接口中声明的speak方法

// 两种鸭子都要实现该方法,所以需要给鸭子这个接口类型写函数
func speak(d Duck){
    d.speak()
}

这样只要调用这个函数,不同的鸭子类型都可以实现speak这个方法

但是当我们想要使用每个鸭子的具体的私有的属性时就不能完成了

这时候我们可以使用类型断言,我们断言他是某个类型的值

func speak(d Duck){
    a := d.(GDuck)  // 断言他是高级鸭子,这样就能使用高级鸭子的属性了
    fmt.Println(a.name)
    a.speak()
}

但是这样断言之后又出现了问题,如果是其他类型使用该函数,断言错误,就不能使用这个函数了

我们可以使用类型选择(Type Switch)来帮助我们完成断言

类型选择用于将接口的具体类型与很多 case 语句所指定的类型进行比较。它与一般的 switch 语句类似。唯一的区别在于类型选择指定的是类型,而一般的 switch 指定的是值。

// 上面这种方法虽然能使用某个鸭子的属性,但是只能使用具体的那一个鸭子,我们可以使用switch
func speak(d Duck){
    switch a:=d.(type) {
    case PDuck:
        fmt.Println(a.name)
        a.speak()
    case GDuck:
        fmt.Println(a.wife)
        a.speak()
    }
}
// 类型断言,使用switch

4.空接口

没有包含方法的接口称为空接口。空接口表示为 interface{}。由于空接口没有方法,因此所有类型都实现了空接口。

package main

import "fmt"

// 空接口:一个方法都没有的接口
// 任何类型都可以使用该接口
type Empty interface {

}

func main() {
    var a = 5
    var b = "sxc"
    var c = map[int]string{3:"sxc"}
    d := 5.95
    //test(a)
    //test(b)
    Mytype(a)
    Mytype(b)
    Mytype(c)
    Mytype(d)
    var e Empty = 6 // 所有类型都赋值给了空接口类型
    fmt.Println(e)
}

func test(e Empty){
    fmt.Println(e)
}

匿名空接口,可以接收任意类型的数据

func Mytype(e interface{}){
    switch a:=e.(type) {
    case int:
        fmt.Println(a,"我是int类型")
    case string:
        fmt.Println(a,"我是sting类型")
    case map[int]string:
        fmt.Println(a,"我是map[int]string类型")
    default:
        fmt.Println(a,"不知道是什么类型")
    }
}

5.指针接受者与值接受者

在接口(一)上的所有示例中,我们都是使用值接受者(Value Receiver)来实现接口的。我们同样可以使用指针接受者(Pointer Receiver)来实现接口。只不过在用指针接受者实现接口时,还有一些细节需要注意。

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\n", p.name, p.age)
}

type Address struct {
    state   string
    country string
}

func (a *Address) Describe() { // 使用指针接受者实现
    fmt.Printf("State %s Country %s", a.state, a.country)
}

func main() {  
    var d1 Describer
    p1 := Person{"Sam", 25}
    d1 = p1
    d1.Describe()
    p2 := Person{"James", 32}
    d1 = &p2
    d1.Describe()

    var d2 Describer
    a := Address{"Washington", "USA"}

    /* 如果下面一行取消注释会导致编译错误:
       cannot use a (type Address) as type Describer
       in assignment: Address does not implement
       Describer (Describe method has pointer
       receiver)
    */
    //d2 = a

    d2 = &a // 这是合法的
    // 因为在第 22 行,Address 类型的指针实现了 Describer 接口
    d2.Describe()

}

在上面程序中的第 13 行,结构体 Person 使用值接受者,实现了 Describer 接口。

我们在讨论方法的时候就已经提到过,使用值接受者声明的方法,既可以用值来调用,也能用指针调用。不管是一个值,还是一个可以解引用的指针,调用这样的方法都是合法的。

p1 的类型是 Person,在第 29 行,p1 赋值给了 d1。由于 Person 实现了接口变量 d1,因此在第 30 行,会打印 Sam is 25 years old

接下来在第 32 行,d1 又赋值为 &p2,在第 33 行同样打印输出了 James is 32 years old。棒棒哒。:)

在 22 行,结构体 Address 使用指针接受者实现了 Describer 接口。

在上面程序里,如果去掉第 45 行的注释,我们会得到编译错误:main.go:42: cannot use a (type Address) as type Describer in assignment: Address does not implement Describer (Describe method has pointer receiver)。这是因为在第 22 行,我们使用 Address 类型的指针接受者实现了接口 Describer,而接下来我们试图用 a 来赋值 d2。然而 a 属于值类型,它并没有实现 Describer 接口。你应该会很惊讶,因为我们曾经学习过,使用指针接受者的方法,无论指针还是值都可以调用它。那么为什么第 45 行的代码就不管用呢?

其原因是:对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口中存储的具体值(Concrete Value)并不能取到地址,因此在第 45 行,对于编译器无法自动获取 a 的地址,于是程序报错。

第 47 行就可以成功运行,因为我们将 a 的地址 &a 赋值给了 d2

程序的其他部分不言而喻。该程序会打印:

Sam is 25 years old  
James is 32 years old  
State Washington Country USA

6.实现多个接口

类型可以实现多个接口。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type Employee struct {  
    firstName string
    lastName string
    basicPay int
    pf int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {  
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {  
    return e.totalLeaves - e.leavesTaken
}

func main() {  
    e := Employee {
        firstName: "Naveen",
        lastName: "Ramanathan",
        basicPay: 5000,
        pf: 200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var s SalaryCalculator = e
    s.DisplaySalary()
    var l LeaveCalculator = e
    fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}

上述程序在第 7 行和第 11 行分别声明了两个接口:SalaryCalculator 和 LeaveCalculator

第 15 行定义了结构体 Employee,它在第 24 行实现了 SalaryCalculator 接口的 DisplaySalary 方法,接着在第 28 行又实现了 LeaveCalculator 接口里的 CalculateLeavesLeft 方法。于是 Employee 就实现了 SalaryCalculator 和 LeaveCalculator 两个接口。

第 41 行,我们把 e 赋值给了 SalaryCalculator 类型的接口变量 ,而在 43 行,我们同样把 e 赋值给 LeaveCalculator 类型的接口变量 。由于 e 的类型 Employee 实现了 SalaryCalculator 和 LeaveCalculator 两个接口,因此这是合法的。

该程序会输出:

Naveen Ramanathan has salary $5200  
Leaves left = 25

7.接口的嵌套

尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。

package main

import (  
    "fmt"
)

type SalaryCalculator interface {  
    DisplaySalary()
}

type LeaveCalculator interface {  
    CalculateLeavesLeft() int
}

type EmployeeOperations interface {  
    SalaryCalculator
    LeaveCalculator
}

type Employee struct {  
    firstName string
    lastName string
    basicPay int
    pf int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {  
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {  
    return e.totalLeaves - e.leavesTaken
}

func main() {  
    e := Employee {
        firstName: "Naveen",
        lastName: "Ramanathan",
        basicPay: 5000,
        pf: 200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var empOp EmployeeOperations = e
    empOp.DisplaySalary()
    fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}

在上述程序的第 15 行,我们创建了一个新的接口 EmployeeOperations,它嵌套了两个接口:SalaryCalculator 和 LeaveCalculator

如果一个类型定义了 SalaryCalculator 和 LeaveCalculator 接口里包含的方法,我们就称该类型实现了 EmployeeOperations 接口。

在第 29 行和第 33 行,由于 Employee 结构体定义了 DisplaySalary 和 CalculateLeavesLeft 方法,因此它实现了接口 EmployeeOperations

在 46 行,empOp 的类型是 EmployeeOperationse 的类型是 Employee,我们把 empOp 赋值为 e。接下来的两行,empOp 调用了 DisplaySalary() 和 CalculateLeavesLeft() 方法。

该程序输出:

Naveen Ramanathan has salary $5200
Leaves left = 25

8.接口的零值

接口的零值是 nil。故接口是引用类型。对于值为 nil 的接口,其底层值(Underlying Value)和具体类型(Concrete Type)都为 nil

package main

import "fmt"

type Describer interface {  
    Describe()
}

func main() {  
    var d1 Describer
    if d1 == nil {
        fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    }
}

上面程序里的 d1 等于 nil,程序会输出:

d1 is nil and has type <nil> value <nil>

对于值为 nil 的接口,由于没有底层值和具体类型,当我们试图调用它的方法时,程序会产生 panic 异常。

package main

type Describer interface {
    Describe()
}

func main() {  
    var d1 Describer
    d1.Describe()
}

在上述程序中,d1 等于 nil,程序产生运行时错误 panic: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527] 。

四.异常处理和错误处理

1.异常处理

异常处理需要知道的三个参数

// 异常处理
// defer:无论如何都会在最后执行
// panic:主动抛出异常
// recover:恢复执行

代码示例

package main

import "fmt"

func main() {
    t1()
    t2()
    t3()
}

func t1() {
    fmt.Println("t1")
}
func t2() {
    defer func() {
        if a:= recover();a!=nil{  // a是错误信息
            fmt.Println(a)  // 打印错误信息
        }
        // finally最后都会执行的话
        fmt.Println("无论如何都会在最后执行")
    }()
    fmt.Println("t2")
    panic("主动抛出异常")
    //var a = []int{3,3}  
    //fmt.Println(a[4])  // 取不到值会报错
    fmt.Println("异常后面的信息")

}
func t3() {
    fmt.Println("t3")
}

故最后异常处理我们只需要写

异常处理范本

defer func() {
    if a:= recover();a!=nil{  // a是错误信息
        fmt.Println(a)  // 打印错误信息
    }
    // finally最后都会执行的话
    fmt.Println("无论如何都会在最后执行")
}()

2.错误处理

错误表示程序中出现了异常情况。比如当我们试图打开一个文件时,文件系统里却并没有这个文件。这就是异常情况,它用一个错误来表示。

代码示例

package main

import (
    "errors"
    "fmt"
)

func main() {
    a,err := circle(-10)
    if err!=nil{
        fmt.Println(err)
    }
    fmt.Println(a)
}

func circle(a int) (int, error){
    if a < 0 {
        return 0,errors.New("出错了")  // 出错返回正确类型的空值
    }
    return 100,nil
}

1.我们只需要在错误的情况中处理错误,并返回需要返回类型的零值和错误信息

2.在主代码中

判断传回来的err值是否为nil,如果不为nil就说明有错误,处理错误

如果为nil就表明没有错误,继续执行代码

105

在上述程序中,d1 等于 nil,程序产生运行时错误 panic: panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8527] 。

猜你喜欢

转载自www.cnblogs.com/sxchen/p/12032310.html