• 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

  • 相关阅读:
    使用 awk 命令统计文本
    Mysql基础及系统函数(分享)
    存储过程 :字段按逗号拆分并插入到关联表
    Spring Security SavedRequestAwareAuthenticationSuccessHandler类
    自制Springboot Starter
    vue3 input中回车生成标签
    NOMURA Programming Contest 2022(AtCoder Beginner Contest 253)
    Educational Codeforces Round 129 (Rated for Div. 2)
    Panasonic Programming Contest 2022(AtCoder Beginner Contest 251)
    2022 Google Kick Start Round C
  • 原文地址:https://www.cnblogs.com/sxchen/p/12032310.html
Copyright © 2020-2023  润新知