基础-2

结构体

  1. 结构体也是一种类型,它可以存储不同的数据类型,定义在函数外部:type 结构体名 struct { }
    type Student struct {
        id int
        name string
        sex byte
    }
    
  2. 结构体是一种数据存储格式,不能在声明结构体时初始化成员,每个成员的值就是所属类型的默认值;
    var stu Student
    fmt.Println(stu)  // {0 "" 0}
    
  3. 结构体的赋值
    1. 通过结构体变量为其成员赋值;
        var stu Student
        stu.id = 10020
        stu.name = "Mack"
        stu.sex = 1
        fmt.Println(stu)  // {10020 Mack 1}
    
    1. 结构体的成员顺序是不变的,匿名初始化时必须初始化结构体成员的所有成员,而指定名称的初始化则不需要;
        var stu Student = Student{10020, "Mack", 1}
        fmt.Println(stu)  // {10020 Mack 1}
    
        var stu Student = Student{name: "Mack"}
    
  4. 结构体的传递是值传递,如果结构体的成员都支持==、!=,那么该结构体也支持==、!=
    stu := Student{10020, "Mack", 1}
    m := stu
    m.id = 2
    fmt.Println(stu)  // {10020 Mack 1}
    fmt.Println(m)  // {2 Mack 1}
    fmt.Println(m==stu)  // false
    
  5. 结构体与数组、切片、字典
    //结构体数组
    var arr [5]Student
    arr3 := [2]Student{{100, "Mack", 1}, {200, "Rose", 0}}
    //结构体切片
    s := []Student{{100, "Mack", 1}, {200, "Rose", 0}}
    //结构体Map
    m := make(map[string]Student)  //key为string型,value为结构体Student
    m["10010"] = {100, "Mack", 1}
    //结构体切片Map
    ms := make(map[string][]Student)  //key为string型,value为切片,且切片元素类型为结构体Student
    ms["10012"] = append(ms["10012"], Student{1, "Mack", 1})
    fmt.Println(ms)  //map[10012:[{1 Mack 1}]]
    
  6. 结构体本身在定义时存储在数据区,其中的成员则根据数据类型存储在不同区域,如数组栈区,切片在堆区。

指针

指针用于存储变量的内存地址,指针的类型与变量的类型保持一致;

  1. 定义:var p *类型,一级指针,指针的默认值为nil(空指针)
    var p *int
    fmt.Println(p)  // 
    var num int = 1
    p = &num
    fmt.Println(p)  // 0xc00000a0c0
    fmt.Printf("%p", &num)  // 0xc00000a0c0
    
  2. * 取值运算符,用于从内存地址中存储的值,通过指针还可以修改内存中的值;
    num := 1
    p := &num
    fmt.Println(*p)  // 1
    
    *p = 22
    fmt.Println(num)  // 22
    
  3. 空指针指向内存编号为 0 的空间,但是内存编号 0-255被系统占用,不允许用户进行读写操作;
    var p *int
    fmt.Println(*p)  // runtime error
    *p = 1  //runtime error
    
  4. 野指针:指向一块未知的空间,与空指针类似,都不允许进行读写操作;
    var p *int
    p = 0x04803c60
    *p = 1  //runtime error
    
  5. 数组指针
    1. 数组存储在一块连续的内存空间,数组指针指向数组的首地址;
        var arr [5]int = [5]int{1,2,3,4,5}
        var p *[5]int = &arr
    
        fmt.Println(p)  // &[1 2 3 4 5] 不会直接打印数组指针指向的地址
    
        fmt.Printf("%p", &arr)  // 0xc00006c060
        fmt.Printf("%p", &arr[0])  // 0xc00006c060
        fmt.Printf("%p", p)     // 0xc00006c060
        fmt.Printf("%p", &arr[1])  // 0xc00006c068
    
    1. 数组指针操作数据元素:(*)p[0]、p[0]
        fmt.Println(*p)  // [1 2 3 4 5]
        
        (*p)[0] = 100   // [] 操作符的优先级高于 *
    
        //Go语言做了优化,使用简写形式
        p[1] = 200
    
        fmt.Println(arr)  // [100 200 3 4 5]
    
        len(p)  // 5 通过指针变量获取数组长度
    
  6. 切片数组
    1. 与数组名不同,切片名本身就是一个地址,且这个地址就是切片存储的堆区内存地址;
        slice := []int{1,2,3,4,5}
        fmt.Printf("%p", slice)      // 0xc00006c060
        fmt.Printf("%p", &slice[0])  // 0xc00006c060
        p := &slice
        fmt.Printf("%p", p)   // 0xc00004c420
    
    1. &slice 获取的是栈区中 slice 变量的地址,而不是切片存储在堆区的地址!
    2. 通过指针操作切片时的查找过程:p -> slice变量地址 -> slice指向的堆区地址
        (*p)[0] = 200
        fmt.Println(slice)  // [200 2 3 4 5]
        fmt.Println(*p)  // [200 2 3 4 5]
    
    1. Go没有对指针访问切片做优化,仍需要使用取地址操作符*
    2. 这种没有直接指向堆区地址的指针,称为二级指针!
    3. 既然指针指向的是切片变量的地址,那么对指针使用append()导致切片扩容时,切片变量指向的堆区地址也会随之变化!
        slice := []int{1,2,3}
        fmt.Printf("%p", slice)  // 0xc00006c060
        p := &slice
        *p = append(*p, 6, 7, 8, 9)
        fmt.Printf("%p", slice)  // 0xc00008a000
        fmt.Println(slice)  // [1 2 3 6 7 8 9]
    
  7. 手动创建指针空间:new(数据类型)
    var p *int
    p = new(int)  // 为指针变量在堆区创建一块内存空间
    fmt.Println(p)  // 0xc000056080
    fmt.Println(*p)  // 0
    
    1. 开辟的空间位于堆区,存储的值是数据类型的默认值,并返回内存地址;
    2. Go语言中无需关心内存空间的释放,交由系统管理.
  8. 指针的传递是地址传递,形参可以改变实参的值;
  9. 指针数组/切片:元素为指针的数组/切片
    a, b := 1, 2
    var arr [2]*int
    arr[0] = &a
    arr[1] = &b
    fmt.Println(arr)  // [0xc00000a098 0xc00000a0b0]
    
    var slice []*int
    slice = append(slice, &a, &b)
    fmt.Println(slice)  // [0xc00000a098 0xc00000a0b0]
    
  10. 结构体指针:也是指向结构体的首地址,还可以简化操作结构体成员
    type Student struct {
        id int
        name string
    }
    func main() {
        var stu Student = Student{1, "Alpa"}
        var p *Student
        p = &stu
        fmt.Printf("%p\n", &stu.id)  // 0xc000004480
        fmt.Printf("%p\n", p)        // 0xc000004480
        
        p.name = "Babel"   // (*p).name = "Babel"
        fmt.Println(stu)  // {1 Babel}
    }
    
  11. 多级指针
    a := 10
    p := &a  // 一级指针
    P2 := &p   // 二级指针
    P3 := &p2   // 三级指针
    

内存模型

  1. 内存是一块连续的一维数组空间:低地址 -> 高地址,其中 0 - 255被系统所占用;
  2. 对于一个应用程序来说,内存可以简单分为四个区域,从低地址到高地址依次为:代码区、数据区、堆区、栈区;
  3. 代码区:存储计算指令信息,只读
  4. 数据区:又可分为常量区、初始化数据区,未初始化数据区,其中常量区不允许显示内存地址,而结构体就位于未初始化数据区;
  5. 堆区:切片数据、string类型数据、new()...
  6. 栈区:局部变量、函数调用时的信息
  7. 它们之间并不是相邻的,中间还有一些小的区域,其中最高地址段分配给了注册表。

面向对象

Go语言中没有类的概念,但可以将结构体比作类,结构体成员比较类的属性、方法;
Go语言中也没有继承,但可以通过 匿名组合 来实现继承的效果。

  1. 匿名字段,又叫匿名组合:用一个结构体的名称作为另一个结构体的成员;
    1. 在初始化子结构体时,可以直接访问父结构体的成员;
        type Person struct {
            name string
            age int
        }
        type Student struct {
            Person
            id int
            score float32
        }
        func main() {
            var stu Student
            stu.id = 101
            stu.name = "Mack"  //直接访问父结构体成员
            stu.Person.age = 20  //间接访问
            stu.score = 60.5
            fmt.Println(stu)  //{{Mack 20} 101 60.5}
        }
    
    1. 定义时的初始化
        var stu Student = Student{Person{"Jack", 17}, 102, 87}
    
    1. 但是,如果子结构体和父结体有同名成员,则采用就近原则:初始化父结构体成员时,必须指定父结构体名称;
        type Person struct {
            name string
            age int
        }
        type Student struct {
            Person
            id int
            name string
            score float32
        }
        func main() {
            var stu Student
            stu.id = 101
            stu.name = "Mack"  //子结构体自己的name
            stu.Person.name = "Jason"  //父结构的name
            stu.age = 13
            stu.score = 60.5
            fmt.Println(stu)  //{{Jason 13} 101 Mack 60.5}
        }
    
  2. 指针匿名字段:继承父结构体的指针
        type Person struct {
            name string
            age int
        }
        type Student struct {
            *Person
            id int
            score float32
        }
    
    1. 指针的默认值是nil,称为空指针,无法直接访问
        var stu Student
        stu.name = "Mack"
        stu.Person.name = "Jason"  //runtime error: invalid memory address or nil...
    
    1. new() 初始化指针变量
        var stu Student
        stu.Person = new(Person)
        stu.Person.name = "Mack"
        stu.age = 20
        fmt.Println(stu.name, stu.age)  // Mack 20
    
    1. 亦或者:先初始化父结构体,再把地址赋与子结构体的指针;
        var p = Person{"Mack", 14}
        var stu Student
        stu.Person = &p
        //定义时初始化
        var stu2 = Student{ &Person{"Mack", 14}, 103, 78.5 }
    
  3. 支持多重继承,但自己不允许继承自己,却可以继承自己的结构体指针,这也是链表的实现原理;
        type Person struct {
            *Person
            name string
            age int
        }
    

方法

  1. Go中的方法和函数是有明显区别的
        func (方法接收者) 方法名(参数列表) 返回值类型 {
            方法体
        }
    
    1. 根据数据类型绑定方法,方法接收者也可以为方法传递参数;
        type Integer int  //为 int 定义别名
        func (a Integer) Test(b Integer) Integer {
            return a+b
        }
        func main() {
            var c Integer = 3
            r := c.Test(3)
            fmt.Println(r)  // 6
        }
    
    1. Go语言不允许方法直接使用数据类型,必须定义别名!
    2. 方法接收者是一种类型,为这种类型绑定了方法之后,此类型的变量都具有该方法!
  2. 结构体也是在为 struct 起别名,那么就可以为结构体绑定方法;
        type Student struct {
            id int
            name string
            age int
        }
        func (s Student) print() {
            fmt.Println(s.name)
        }
        func (s Student) edit(name string) {
            s.name = name
            s.print()  // 调用另一个方法
        }
        func main() {
            s := Student{ 101, "Mart", 20}
            s.edit("Jose")  // Jose
        }
    
    1. 结构体指针访问成员时,支持简化操作,访问方法时也支持简化操作;
        func (s *Student) edit(name string) {
            s.name = name
            s.print()  // (*s).edit("Jose")
        }
        func main() {
            s := Student{ 101, "Mart", 20}
            s.edit("Jose")  // (&s).edit("Jose")
    
            var s2 *Student  //空指针
            s2 = new(Student)  // 为指针s2 开启空间
            s2.edit("Jose")  // Jose
        }
    
    1. 考虑到结构体是值传递,所以都应该使用结构体指针作为接收者;
    2. 结构体中不允许出现同名方法,即使一个方法接收者是结构体名Student、另一个接收者是结构体指针*Student,也不允许!
  3. 方法继承:匿名字段也可以实现方法的继承,同名方法也遵循就近原则;
  4. 方法名允许与函数名重名,但在同级别文件中可以直接调用函数,却不能调用方法,必须先定义结构体;
  5. 方法的本质还是函数,类型都是func(),也都存储在代码区。

接口

  1. 定义
    type 接口名 interface {
        方法列表  //只有声明,没有具体实现
    }
    
  2. 对象(结构体)必须实现所有的接口方法,接口指向对象的内存地址,通过接口调用对象实现的方法,这就是多态;
    type Humaner interface {
        say()
    }
    type Student struct {
        name string
        age int
    }
    func (s *Student) say() {
        fmt.Printf("Student: %s", s.name)
    }
    type Teacher struct {
        name string
        age int
    }
    func (t *Teacher) say() {
        fmt.Printf("Teacher: %s", t.name)
    }
    func main() {
        var stu Student = Student{ "Mack", 20 }
        var h Humaner
        h = &stu
        h.say()  // Student: Mack
        h = &Teacher{ "Eason", 46 }
        h.say()  // Teacher: Eason
    }
    
  3. 接口也可以通过匿名字段实现继承,且接口中的形参可以省略参数名;
    type Humaner interface {
        Say()
    }
    type Male interface {
        Humaner
        Dance(string)   //形参可以省略参数名
    }
    type Student struct {
        name string
    }
    func (s *Student) Say() {
        fmt.Printf("%s say", s.name)
    }
    func (s *Student) Dance(label string) {
        fmt.Printf("dance: %s", label)
    }
    func main() {
        var m Male
        m = &Student{ "Mack" }
        m.Say()  // Mack say
        m.Dance("AAA")  // dance: AAA
    
        var h Humaner
        h =  &Student{ "Eason" }
        h.Say()  // Eason say
    }
    
  4. 接口的转换:允许父接口指向子接口,那么父接口在调用方法时,指向的是针对子接口的实现方法;
        h = m
        h.Say()  // Mack say
    
  5. 空接口:var a interface{}
    1. 空接口可以接收任意类型的数据,但空接口的地址不变!
        var a interface{}
        fmt.Printf("%p", &a)  // 0xc0000501c0
        a = 10
        fmt.Printf("%p", &a)  // 0xc0000501c0
        a = "Hello Go"
        fmt.Printf("%p", &a)  // 0xc0000501c0
        a = false
        fmt.Printf("%p", &a)  // 0xc0000501c0
    
    1. 空接口切片
        var a []interface{}
        a = append(a, 12, "Hello", [3]int{1,2,3})
        fmt.Println(a)  // [12 Hello [1 2 3]]
    
        b := make([]interface{}, 3)
    
  6. 类型断言:类型判断
        var b []interface{}
        b = append(a, 12, "Hello", [3]int{1,2,3})
        for _,v:=range b {
            data,ok := v.(int)  // 判断元素v 是不是 int 类型
        }
    
    1. ok 表示判断结果,如果是,则为true,否则为false;
    2. data 表示判断类型的值,当前判断是不是int类型,如果是,data 值为元素值,否则为int类型的默认值0;
        for _,v:=range b {
            if data,ok := v.(int); ok {  // ok 为 true 时,则进入此分支
            } else if  data,ok := v.([3]int); ok {
            }
        }
    

你可能感兴趣的:(基础-2)