• 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
    ", 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("
    Leaves 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("
    Leaves 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
    ", 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

  • 相关阅读:
    [Python]解决ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out
    Objective C XPC 初步学习<一>
    Vue的渣渣成长之路 第一章 登陆界面(vue+element+axios)以下文章感谢璋,威的同事给了我很大的帮助
    vue详情 恢复 删除
    vue添加
    vue显示详情加入回收站
    linq修改单条数据
    linq详情
    linq显示
    8.11模拟总结
  • 原文地址:https://www.cnblogs.com/sxchen/p/12032310.html
Copyright © 2020-2023  润新知