全栈开发知识库

Golang指针、结构体及接口(六)

指针

一个指针可以指向任何一个值的内存地址,它指向那个值的内存地址,在32位机器上占用4个字节,在64位机器上占用8个字节, 并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性; 你可以在指针类型前面加上*号来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。

在Go语言中,直接砍掉了C语言指针最复杂的指针运算部分,只留下了获取指针(&运算符)和获取对象(*运算符)的运算, 用法和C语言很类似。
Go语言中一个指针被定义后没有分配到任何变量时,它的值为nil。

Go语言自带指针隐式解引用:对于一些复杂类型的指针,如果要访问成员变量时候需要写成类似*p.field的形式时, 只需要p.field即可访问相应的成员。

每个变量都是一个内存位置,每个内存位置都有其定义的地址,可以使用符号(&)运算符来访问它, 这个运算符表示内存中的地址。参考下面的例子,它将打印定义的变量的地址:

package main

import "fmt"

func main() {
   var a int = 10   

   fmt.Printf("Address of a variable: %x\n", &a  ) // Address of a variable: c420014080
}

指针是一个变量,其值是另一个变量的地址,即存储器位置的直接地址。类似变量或常量一样,必须要先声明一个指针,然后才能使用它来存储任何变量地址。 指针变量声明的一般形式是: 类型*T是指向类型T的值的指针。指针变量声明的一般形式是:

var var-name *var-type

这里,var-type是指针的基类型; 它必须是有效的Go数据类型,var-name是指针变量的名称。 用于声明指针的星号(*)与用于乘法的星号相同。但是,在此语句中,星号(*)用于将变量指定为指针。

package main

import "fmt"

func main() {
   var  ptr *int

   fmt.Printf("The value of ptr is : %x\n", ptr  )
}

当上面的代码编译和执行时,它产生结果是The value of ptr is 0,
在大多数操作系统上,程序不允许访问地址0处的内存,因为该内存是由操作系统保留的。然而存储器地址0具有特殊意义; 它表示指针不打算指向可访问的存储器位置。 但是按照惯例,如果指针包含nil(零)值,则假设它不指向任何东西。

要检查是否为nil指针,可以使用if语句,如下所示:

if(ptr != nil)     /* succeeds if p is not nil */
if(ptr == nil)    /* succeeds if p is null */

&符号会生成一个指向其作用对象的指针。

i := 42
p = &i

*符号表示指针指向的底层的值。

fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“非直接引用”。 与C不同,Go没有指针运算。

package main

import "fmt"

func main() {
    i, j := 42, 2701

    p := &i         // point to i
    fmt.Println(*p) // read i through the pointer
    *p = 21         // set i through the pointer
    fmt.Println(i)  // see the new value of i

    p = &j         // point to j
    *p = *p / 37   // divide j through the pointer
    fmt.Println(j) // see the new value of j
}

new函数

使用new函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:

var t *T = new(T)

变量t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值。

package main

import (
    "fmt"
)

type Student struct {
    name   string
    age    int
    weight float32
    score  []int
}

func main(){
    //使用new关键字创建一个指针
    pp := new(Student)                                  
    *pp = Student{"xiaoming", 23, 65.0, []int{2, 3, 6}}
    fmt.Printf("stu pp have %d subjects\n", len((*pp).score))
    fmt.Printf("stu pp have %d subjects\n", len(pp.score)) //Go语言自带隐式解引用
}

执行结果:

stu pp have 3 subjects
stu pp have 3 subjects

我们只需要使用new()函数,无需担心其内存的生命周期或怎样将其删除,因为Go语言的内存管理系统会帮我们打理一切。

func swap(p1, p2 *int) {
 *p1, *p2 = *p2, *p1
}
func main() {
    var a, b = 10, 20
    swap(&a, &b)
    fmt.Println("a = ", a, " b = ", b)
}

结构体

可以理解成是java中的类。
要定义结构体,必须使用type和struct语句。struct语句定义了一个新的数据类型,在程序中有多个成员。 type语句在例子中绑定一个类型为struct的名字。struct语句的格式如下:

type Student struct {
    name   string
    age    int
    weight float32
    score  []int
}

要想调用结构体中的字段直接使用使用点号来访问。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    fmt.Println(Vertex{1, 2}) // {1 2}
    v := Vertex{1, 2}
    v.X = 4
    fmt.Println(v.X)    // 4
}

结构体字段可以通过结构体指针来访问。 通过指针间接的访问是透明的。

package main

import (
    "fmt"
)

type Student struct {
    name   string
    age    int
    weight float32
    score  []int
}

func main() {
    //按照字段顺序进行初始化
    stu01 := Student{"xiaomi", 23, 55.5, []int{95, 96, 98}}                
    //通过 field:value 形式初始化,该方式可以自定义初始化字段的顺序
    stu02 := Student{age: 23, weight: 55.5, score: []int{97, 98}, name: "xiaohong"} 
    stu01.age = 25
    fmt.Println(stu01.age)
    fmt.Println(stu02)
}

结构体符文表示通过结构体字段的值作为列表来新分配一个结构体。 使用Name:语法可以仅列出部分字段。(字段名的顺序无关)。 特殊的前缀&返回一个指向结构体的指针。

package main

import "fmt"

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // 类型为 Vertex
    v2 = Vertex{X: 1}  // Y:0 被省略
    v3 = Vertex{}      // X:0 和 Y:0
    p  = &Vertex{1, 2} // 类型为 *Vertex
)

func main() {
    fmt.Println(v1, p, v2, v3) // {1 2} &{1 2} {1 0} {0 0}
}
  • 通过指针操作结构体
var s Student
var p1 *Student
p1 = &s

// 通过指针操作成员 p1.id 和 (*p1).id完全等价

p1.id = 1
(*p1).name = "hello"

结构体new函数

一般在进行例如type T struct {a, b int}的结构体定义之后,习惯使用t := new(T)给该结构体变量分配内存,它返回指向已分配内存的指针。 变量t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值。
声明var t T也会给t分配内存,并零值化内存,但是这个时候t是类型T。在这两种方式中,t通常被称做类型T的一个实例(instance)或对象(Object)。

一个简单的结构体new函数的例子如下:

package main
import "fmt"

type struct1 struct {
    i1  int
    f1  float32
    str string
}

func main() {
    ms := new(struct1) // ms是结构体struct1的指针
    ms.i1 = 10
    ms.f1 = 15.5
    ms.str= "Chris"

    fmt.Printf("The int is: %d\n", ms.i1)
    fmt.Printf("The float is: %f\n", ms.f1)
    fmt.Printf("The string is: %s\n", ms.str)
    fmt.Println(ms)
}

执行结果:

The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}

结构体的比较

使用==比较和java中比较的内存地址不一样,这里==比较的是里面的元素是否相同

// 伪代码
s1 := Student{1, "hello"}
s2 := Student{1, "hello"}
s3 := Student{2, "hello"}

s1 == s2 // true
s1 == s3 // false

结构体作为参数

结构体作为参数时是值传递,形参无法改变实参数

func main() {
    s := Student{1, "hello"}
    test(s)
    fmt.Println("main :", s)
}

func test(s Student) {
    s.id = 666
    fmt.Println("test 01 :", s)
}

执行结果:

test 01 : {666 hello}
main : {1 hello}

所以如果想要修改原数据,就必须通过引用传递的方式,也就是把指针传递过去 也就是说地址传递(引用传递),形参可以改实参数

func main() {
    s := Student{1, "hello"}
    test(&s)
    fmt.Println("main :", s)
}

func test(s *Student) {
    s.id = 666
    fmt.Println("test 01 :", s)
}

执行结果:

test 01 : &{666 hello}
main : {666 hello}

匿名字段

结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的, 此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。

type Person struct {
    name string
    age int
}
type Student struct {
    Person // 只有类型,没有名字就叫做匿名字段,继承了Person里面的成员
    class string
    byte // 性别 'm' 'f'
}

func main() {
    s := Student{Person{"hello", 1}, "幼儿园", 'm'}
    fmt.Println("s : ", s)
}

输出结果:

s :  {{hello 1} 幼儿园, 109}

通过类型outer.int的名字来获取存储在匿名字段中的数据,于是可以得出一个结论: 在一个结构体中对于每一种数据类型只能有一个匿名字段。

内嵌结构体

同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。 外层结构体通过outer.in1直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。

在其他语言中,有继承可以做同样的事情,但是在Go语言中,没有继承的概念,Go提倡的代码复用的方式是组合,所以这也是嵌入类型的意义所在,组合而不是继承,所以Go才会更灵活。 内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分 或全部实现。

package main

import "fmt"

type A struct {
    ax, ay int
}

type B struct {
    A
    bx, by float32
}

func main() {
    b := B{A{1, 2}, 3.0, 4.0}
    fmt.Println(b.ax, b.ay, b.bx, b.by)
    fmt.Println(b.A)
}

执行结果:

1 2 3 4
{1 2}

同样这种嵌入类型也可以使用到接口中:

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}
type ReadWriter interface {
    Reader
    Writer
}
type ReadCloser interface {
    Reader
    Closer
}
type WriteCloser interface {
    Writer
    Closer
}

以上是标准库io包里,我们常用的接口,可以看到ReadWriter接口是嵌入Reader和Reader接口而组合成的新接口,这样我们就不用重复的定义被嵌入接口里的方法,直接通过嵌入就可以了。

因为Go中没有像java中的private,public这种关键字,所以如果想要使用别的包中的函数、结构体类型、结构体成员,函数名、类型名、结构体成员变量名,首字母必须大写,才可以,如果首字母是小写,只能在同一个包中使用。

接口

Go编程提供了另一种称为接口(interfaces)的数据类型,它代表一组方法签名。 定义了一个接口之后,一般使用一个自定义结构体(struct)去实现接口中的方法。在Go语言中,任何实现了接口的函数的类型,都可以看作是接口的一个实现。 在Go中,如果你这个类型(基本类型,结构体等都可以)拥有了接口的所有方法,那么就默认为这个类型实现了这个接口,是隐式的,不需要和java一样,强制使用implement强制实现。

/* define an interface */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* define a struct */
type struct_name struct {
   /* variables */
}

/* implement interface methods*/
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* method implementation */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* method implementation */
}

示例:

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct{}
type IPhone struct{}

// NokiaPhone结构体实现Phone接口的call方法
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

// IPhone结构体实现Phone接口的call方法
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = NokiaPhone{}
    phone.call()

    phone = IPhone{}
    phone.call()
}

执行结果:

I am Nokia, I can call you!
I am iPhone, I can call you!

接口的继承

接口的继承也是通过匿名字段的方式实现的。

type Humaner interface {
    sayHi()
}

type Personer interface {
    Humaner // Person就继承了Humaner里面的所有方法
    sing()
}

type Student struct {
    name string
    id int
}

func (stu Student) sayHi() {
    fmt.Println("say hi")
}

func (stu Student) sing() {
    fmt.Println("听天使在唱歌")
}

func main() {
    var i Personer
    s := Student{"hello", 1}
    i = s
    i.sayHi() // 继承过来的
    i.sing() 
}

空接口

空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。它有点类似于C语言的void *类型

var v1 interface{} = 1
var v2 interface{] = "abc"
var v3 interface{} = &v2
var v4 interface{}  = struct{X int}{1}
// 我们的打印函数就是一个可变的空接口类型,因为它可以打印任意类型
func Println(args ...interface{})

  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记