66

玩转Golang之Struct结构体

 5 years ago
source link: https://studygolang.com/articles/14133?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

先介绍一下go语言的类型系统

Golang中的类型系统

类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:

 基础类型,如byte、int、bool、float等;

 复合类型,如数组、结构体、指针等;

 可以指向任意对象的类型(Any类型);

 值语义和引用语义;

 面向对象,即所有具备面向对象特征(比如成员方法)的类型;

 接口。

Go语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。而在实现某个接口时,无需从 该接口继承(事实上,Go语言根本就不支持面向对象思想中的继承语法),只需要实现该接口 要求的所有方法即可。任何类型都可以被Any类型引用。Any类型就是空接口,即interface{}。

什么是结构体

结构体(struct)是用户自定义的类型,它代表若干字段的集合,可以用于描述一个实体对象,类似java中的class,是golang面向对象编程的基础类型。

结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type)。在 C++ 它也存在,并且名字也是 struct,在面向对象的编程语言中,跟一个无方法的轻量级类一样。因为 Go 语言中没有类的概念,所以在 Go 中结构体有着更为重要的地位。

如何定义一个结构体

type Coordinate struct {
    X, Y float32
}

语法: type<Name>struct{}
上述代码定义个一个名为 Coordinate 的结构体,里面包括了两个float32的变量 X , Y ,该结构体可用于表示一个平面坐标。

添加对象方法

其他的经典面向对象语言,如java,C#,定义对象方法时,会包含在class的定义内,如

public class Coordinate{
    public float X {get; set;}
    public float Y {get; set;}
    //打印坐标
    public void GetCoordinate(){
          Console.WriteLine("("+this.X+","+this.Y+")");
    }
}

在go语言中,对象方法在结构体定义的外部添加

type Coordinate struct {
    X, Y float32
}
//打印坐标
func (coo *Coordinate) GetCoordinate() {
    fmt.Printf("(%.2f,%.2f)\n", coo.X, coo.Y)
    return
}

其中, func 关键字后面的 (coo *Coordinate) ,表示该函数传入一个指向 Coordinate 的指针,可通过指针变量 coo 来操作结构体的值。

几种结构体初始化

一、按原始字段顺序通过创建结构体

package main
import (
    "fmt"
)
func main(){
    p0 := Coordinate{1, 2}
    p0.GetCoordinate()
}

输出: (1.00,2.00) ,其中X=1,Y=2

二、按照自定义字段顺序进行初始化

package main
import (
    "fmt"
)
func main(){
    p0 := Coordinate{Y:1, X:2}
    p0.GetCoordinate()
}

输出: (2.00,1.00) ,其中X=2,Y=1

三、通过new函数创建

package main
import (
    "fmt"
)
func main(){
    //给该结构体p2变量分配内存,它返回指向已分配内存的指针
    p0 := new(Coordinate)
    p0.X=1
    p0.Y=2
    p0.GetCoordinate()
}

输出: (1.00,2.00) ,其中X=1,Y=2

其中 p0 := new(Coordinate) 等价于以下写法

p3 := &Coordinate{X: 1, Y: 2}
p3 := &Coordinate{1,2}

比较三种创建方式

其中,第一种与第二种, p0 均为一个类型为 Coordinate 的实例,而第三种 p0 为一个指向 Coordinate 的指针,相当于 var p0 *Coordinate = new(Coordinate)

一般在进行例如 type T struct {a, b int} 的结构体定义之后

习惯使用 t := new(T) 给该结构体变量分配内存,它返回指向已分配内存的指针。变量 t 是一个指向 T 的指针,此时结构体字段的值是它们所属类型的零值。

声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t是类型T。在这两种方式中,t 通常被称做类型T的一个实例 (instance) 或对象 (Object)var t *T = new(T) 等价于 t := new(T)

通过代码分析以上结论

package main
import (
    "fmt"
)
func main(){
    p0 := Coordinate{1, 2}
    //给该结构体p2变量分配内存,它返回指向已分配内存的指针
    p2 := new(Coordinate)
    p2.X = 1
    p2.Y = 2
    p3 := &Coordinate{X: 1, Y: 2}
    p4 := &Coordinate{1, 2}

    fmt.Println("-------输出p0-------")
    fmt.Printf("%v\n%T\n", p0, p0)
    fmt.Println("-------输出p2-------")
    fmt.Printf("%v\n%T\n", p2, p2)
    fmt.Println("-------输出p3-------")
    fmt.Printf("%v\n%T\n", p3, p3)
    fmt.Println("-------输出p4-------")
    fmt.Printf("%v\n%T\n", p4, p4)
}

输出:

-------输出p0-------
{1 2}
Coordinate
-------输出p2-------
&{1 2}
*Coordinate
-------输出p3-------
&{1 2}
*Coordinate
-------输出p4-------
&{1 2}
*Coordinate

可以看出来,p2,p3,p4均为一个指针变量

添加值拷贝的对象方法

刚才说到了,添加一个对象方法,可以通过 func (t *T) functionname() 来创建,其中 t 为一个指针变量。我们也可以通过值拷贝的方式,添加一个对象方法,语法为 func(t T) functionname()

package main
import (
    "fmt"
)
type Coordinate struct {
    X, Y float32
}

func (coo *Coordinate) GetCoordinate() {
    fmt.Printf("(%.2f,%.2f)\n", coo.X, coo.Y)
    return
}
//值拷贝对象方法
func (coo Coordinate) SetPosition01(a float32,b float32) {
    coo.X = a
    coo.Y = b
}

//指针变量对象方法
func (coo *Coordinate) SetPosition02(a float32,b float32) {
    coo.X = a
    coo.Y = b
}
func main(){
    p0 := Coordinate{1, 2}
    fmt.Print("SetPosition02调用前:")
    p0.GetCoordinate()
    p0.SetPosition02(0, 0)
    fmt.Print("SetPosition02调用后:")
    p0.GetCoordinate()
}

输出:

SetPosition01调用前:(1.00,2.00)
SetPosition01调用后:(1.00,2.00)
SetPosition02调用前:(1.00,2.00)
SetPosition02调用后:(0.00,0.00)

从程序输出中可以看出,调用 SetPosition01 方法,发生了值拷贝,即使在方法内改变了 coo 的值,外部的 p0 的值没有被改变。而 SetPosition02 方法中, coo 为指向 p0 地址的指针,由于是通过指针变量修改了 X,Y 的值,所以调用完毕后,外部 p0 的值会被修改为 (0,0)

匿名结构体

package main
import (
    "fmt"
)
func main(){
    p_3d := struct {
        X, Y, Z float32
    }{1, 2, 3}
    fmt.Println("-------输出p_3d-------")
    fmt.Printf("%v\n%T\n", p_3d, p_3d)
}

输出:

-------输出p_3d-------
{1 2 3}
struct { X float32; Y float32; Z float32 }

p_3d 为一个包含 X,Y,Z 三个变量的匿名结构体

golang构造函数?

在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以 NewXXX 来命名,表示“构造函数”:

这一切非常自然,开发者也不需要分析在使用了new之后到底背后发生了多少事情。在Go语言中,一切要发生的事情都直接可以看到。

—— 《Go语言编程》

func NewRect(x, y, width, height float64) *Rect { 
    return &Rect{x, y, width, height}
}

变量、方法可见性

Go语言对关键字的增加非常吝啬,其中没有 privateprotectedpublic 这样的关键 字。要使某个符号对其他包( package )可见(即可以访问),需要将该符号定义为以大写字母开头,如:

type Rect struct { 
  X, Y float64
  Width, Height float64 
}

这样,Rect类型的成员变量就全部被导出了,可以被所有其他引用了Rect所在包的代码访问到。 成员方法的可访问性遵循同样的规则,例如:

func (r *Rect) area() float64 { 
    return r.Width * r.Height
}

这样, Rectarea() 方法只能在该类型所在的包内使用。

需要注意的一点是,Go语言中符号的可访问性是包一级的而不是类型一级的。在上面的例 子中,尽管 area()Rect 的内部方法,但同一个包中的其他类型也都可以访问到它。这样的可访问性控制很粗旷,很特别,但是非常实用。如果Go语言符号的可访问性是类型一级的,少不 了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问彼此的私有成员。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK