go基础系列:结构struct

时间:2021-10-17 22:02:16

Go语言不是一门面向对象的语言,没有对象和继承,也没有面向对象的多态、重写相关特性。

Go所拥有的是数据结构,它可以关联方法。Go也支持简单但高效的组合(Composition),请搜索面向对象和组合。

虽然Go不支持面向对象,但Go通过定义数据结构的方式,也能实现与Class相似的功能。

go基础系列:结构struct

一个简单的例子,定义一个Animal数据结构:

type Animal struct {
name string
speak string
}

这就像是定义了一个class,有自己的属性。

go基础系列:结构struct

在稍后,将会介绍如何向这个数据结构中添加方法,就像为类定义方法一样。不过现在,先简单介绍下数据结构。

数据结构的定义和初始化

除了int、string等内置的数据类型,我们可以定义structure来自定义数据类型。

创建数据结构最简单的方式:

bm_horse := Animal{
name:"baima",
speak:"neigh",
}

注意,上面最后一个逗号","不能省略,Go会报错,这个逗号有助于我们去扩展这个结构,所以习惯后,这是一个很好的特性。

上面bm_horse := Animal{}中,Animal就像是一个类,这个声明和赋值的操作就像创建了一个Animal类的实例,也就是对象,其中对象名为bm_horse,它是这个实例的唯一标识符。这个对象具有属性name和speak,它们是每个对象所拥有的key,且它们都有自己的值。从面向对象的角度上考虑,这其实很容易理解。

还可以根据Animal数据结构再创建另外一个实例:

hm_horse := Animal{
name:"heima",
speak:"neigh",
}

bm_horsehm_horse都是Animal的实例,根据Animal数据结构创建而来,这两个实例都拥有自己的数据结构。如下图:

go基础系列:结构struct

从另一种角度上看,bm_horse这个名称其实是这个数据结构的一个引用。再进一步考虑,其实面向对象的类和对象也是一种数据结构,每一个对象的名称(即bm_horse)都是对这种数据结构的引用。关于这一点,在后面介绍指针的时候将非常有助于理解。

以下是两外两种有效的数据结构定义方式:

// 定义空数据结构
bm_horse := Animal{} // 或者,先定义一部分,再赋值
bm_horse := Animal {name:"baima"}
bm_horse.speak = "neigh"

此外,还可以省略数据结构中的key部分(也就是属性的名称)直接为数据结构中的属性赋值,只不过这时赋的值必须和key的顺序对应。

bm_horse := Animal{"baima","neigh"}

在数据结构的属性数量较少的时候,这种赋值方式也是不错的,但属性数量多了,不建议如此赋值,因为很容易混乱。

访问数据结构的属性

要访问一个数据结构中的属性,如下:

package main

import ("fmt")

func main(){

    type Animal struct {
name string
speak string
} bm_horse := Animal{"baima","neigh"}
fmt.Println("name:",bm_horse.name)
fmt.Println("speak:",bm_horse.speak)
}

前面说过,Animal是一个数据结构的模板(就像类一样),不是实例,bm_horse才是具体的实例,有自己的数据结构,所以,要访问自己数据结构中的数据,可以通过自己的名称来访问自己的属性:

bm_horse.name
bm_horse.speak

指针

bm_horse := Animal{}表示返回一个数据结构给bm_horse,bm_horse指向这个数据结构,也可以说bm_horse是这个数据结构的引用。

除此,还有另一种赋值方式,比较下两种赋值方式:

bm_horse := Animal{"baima","neigh"}
ref_bm_horse := &Animal{"baima","neigh"}

这两种赋值方式,有何不同?

:=操作符都声明左边的变量,并赋值变量。赋值的内容基本神似:

  • 第一种将整个数据结构赋值给变量bm_horsebm_horse从此变成Animal的实例;
  • 第二种使用了一个特殊符号&在数据结构前面,它表示返回这个数据结构的引用,也就是这个数据结构的地址,所以ref_bm_horse也指向这个数据结构。

bm_horseref_bm_horse都指向这个数据结构,有什么区别?

实际上,赋值给bm_horse的是Animal实例的地址,赋值给ref_bm_horse是一个中间的指针,这个指针里保存了Animal实例的地址。它们的关系相当于:

bm_horse -> Animal{}
ref_bm_horse -> Pointer -> Animal{}

其中Pointer在内存中占用一个长度为一个机器字长的单独数据块,64位机器上一个机器字长是8字节,所以赋值给ref_bm_horse的这个8字节长度的指针地址,这个指针地址再指向Animal{},而bm_horse则是直接指向Animal{}

如果还不明白,我打算用perl语言的语法来解释它们的区别,因为C和Go的指针太过"晦涩"。

perl中的引用

在Perl中,一个hash结构使用%符号来表示,例如:

%Animal = (
name => "baima",
speak => "neigh",
);

这里的"Animal"表示的是这个hash结构的名称,然后通过%+NAME的方式来引用这个hash数据结构。其实hash结构的名称"Animal"就是这个hash结构的一个引用,表示指向这个hash结构,只不过这个Animal是创建hash结构是就指定好的已命名的引用。

perl中还支持显式地创建一个引用。例如:

$ref_myhash = \%Animal;

%Animal表示的是hash数据结构,加上\表示这个数据结构的一个引用,这个引用指向这个hash数据结构。perl中的引用是一个变量,所以使用$ref_myhash表示。

go基础系列:结构struct

也就是说,hash结构的名称Animal$ref_myhash是完全等价的,都是hash结构的引用,也就是指向这个数据结构,也就是指针。所以,%Animal能表示取hash结构的属性,%$ref_myhash也能表示取hash结构的属性,这种从引用取回hash数据结构的方式称为"解除引用"。

另外,$ref_myhash是一个变量类型,而%Animal是一个hash类型。

引用变量可以赋值给另一个引用变量,这样两个引用都将指向同一个数据结构:

$ref_myhash1 = $ref_myhash;

现在,$ref_myhash$ref_myhash1Animal都指向同一个数据结构。

Go中的指针:引用

总结下上面perl相关的代码:

%Animal = (
name => "baima",
speak => "neigh",
); $ref_myhash = \%Animal;
$ref_myhash1 = $ref_myhash;

%Animal是hash结构,Animal$ref_myhash$ref_myhash1都是这个hash结构的引用。

回到Go语言的数据结构:

bm_horse :=  Animal{}
hm_horse := &Animal{}

这里的Animal{}是一个数据结构,相当于perl中的hash数据结构:

(
name => "baima",
speak => "neigh",
)

bm_horse是数据结构的直接赋值对象,它直接表示数据结构,所以它等价于前面perl中的%Animal。而hm_horseAnimal{}数据结构的引用,它等价于perl中的Animal$ref_myhash$ref_myhash1

之所以Go中的指针不好理解,就是因为数据结构bm_horse和引用hm_horse都没有任何额外的标注,看上去都像是一种变量。但其实它们是两种不同的数据类型:一种是数据结构,一种是引用。

Go中的星号"*"

星号有两种用法:

  • x *int表示变量x是一个引用,这个引用指向的目标数据是int类型。更通用的形式是x *TYPE
  • *x表示x是一个引用,*x表示解除这个引用,取回x所指向的数据结构,也就是说这是 一个数据结构,只不过这个数据结构可能是内置数据类型,也可能是自定义的数据结构

x *int的x是一个指向int类型的引用,而&y返回的也是一个引用,所以&y的y如果是int类型的数据,&y可以赋值给x *int的x。

注意,x的数据类型是*int,不是int,虽然x所指向的是数据类型是int。就像前面perl中的引用只是一个变量,而其指向的却是一个hash数据结构一样。

*x代表的是数据结构自身,所以如果为其赋值(如*x = 2),则新赋的值将直接保存到x指向的数据中。

例如:

package main

import ("fmt")

func main(){
var a *int
c := 2
a = &c
d := *a
fmt.Println(*a) // 输出2
fmt.Println(d) // 输出2
}

var a *int定义了一个指向int类型的数据结构的引用。a = &c中,因为&c返回的是一个引用,指向的是数据结构c,c是int类型的数据结构,将其赋值给a,所以a也指向c这个数据结构,也就是说*a的值将等于2。所以d := *a赋值后,d自身是一个int类型的数据结构,其值为2。

package main

import "fmt"

func main() {
var i int = 10
println("i addr: ", &i) // 数据对象10的地址:0xc042064058 var ptr *int = &i
fmt.Printf("ptr=%v\n", ptr) // 0xc042064058
fmt.Printf("ptr addr: %v\n", &ptr) // 指针对象ptr的地址:0xc042084018
fmt.Printf("ptr地址: %v\n", *&ptr) // 指针对象ptr的值0xc042064058
fmt.Printf("ptr->value: %v", *ptr) // 10
}

Go函数参数传值

Go函数给参数传递值的时候是以复制的方式进行的

因为复制传值的方式,如果函数的参数是一个数据结构,将直接复制整个数据结构的副本传递给函数,这有两个问题:

  1. 函数内部无法修改传递给函数的原始数据结构,它修改的只是原始数据结构拷贝后的副本
  2. 如果传递的原始数据结构很大,完整地复制出一个副本开销并不小

例如,第一个问题:

package main

import ("fmt")

type Animal struct {
name string
weight int
} func main(){
bm_horse := Animal{
name: "baima",
weight: 60,
}
add(bm_horse)
fmt.Println(bm_horse.weight)
} func add(a Animal){
a.weight += 10
}

上面的输出结果仍然为60。add函数用于修改Animal的实例数据结构中的weight属性。当执行add(bm_horse)的时候,bm_horse传递给add()函数,但并不是直接传递给add()函数,而是复制一份bm_horse的副本赋值给add函数的参数a,所以add()中修改的a.weight的属性是bm_horse的副本,而不是直接修改的bm_horse,所以上面的输出结果仍然为60。

为了修改bm_horse所在的数据结构的值,需要使用引用(指针)的方式传值。

只需修改两个地方即可:

package main

import ("fmt")

type Animal struct {
name string
weight int
} func main(){
bm_horse := &Animal{
name: "baima",
weight: 60,
}
add(bm_horse)
fmt.Println(bm_horse.weight)
} func add(a *Animal){
a.weight += 10
}

为了修改传递给函数参数的数据结构,这个参数必须是直接指向这个数据结构的。所以使用add(a *Animal),既然a是一个Animal数据结构的一个实例的引用,所以调用add()的时候,传递给add()中的参数必须是一个Animal数据结构的引用,所以bm_horse的定义语句中使用&符号。

当调用到add(bm_horse)的时候,因为bm_horse是一个引用,所以赋值给函数参数a时,复制的是这个数据结构的引用,使得add能直接修改其外部的数据结构属性。

大多数时候,传递给函数的数据结构都是它们的引用,但极少数时候也有需求直接传递数据结构。

方法:属于数据结构的函数

可以为数据结构定义属于自己的函数。

package main
import ("fmt") type Animal struct {
name string
weight int
} func (a *Animal) add() {
a.weight += 10
} func main() {
bm_horse := &Animal{"baima",70}
bm_horse.add()
fmt.Println(bm_horse.weight) // 输出80
}

上面的add()函数定义方式func (a *Animal) add(){},它所表示的就是定义于数据结构Animal上的函数,就像类的实例方法一样,只要是属于这个数据结构的实例,都能直接调用这个函数,正如bm_horse.add()一样。

构造器

面向对象中有构造器(也称为构造方法),可以根据类构造出类的实例:对象。

Go虽然不支持面向对象,没有构造器的概念,但也具有构造器的功能,毕竟构造器只是一个方法而已。只要一个函数能够根据数据结构返回这个数据结构的一个实例对象,就可以称之为"构造器"。

例如,以下是Animal数据结构的一个构造函数:

func newAnimal(n string,w int) *Animal {
return &Animal{
name: n,
weight: w,
}
}

以下返回的是非引用类型的数据结构:

func newAnimal(n string,w int) Animal {
return Animal{
name: n,
weigth: w,
}
}

一般上面的方法类型称为工厂方法,就像工厂一样根据模板不断生成产品。但对于创建数据结构的实例来说,一般还是会采用内置的new()方式。

new函数

尽管Go没有构造器,但Go还有一个内置的new()函数用于为一个数据结构分配内存。其中new(x)等价于&x{},以下两语句等价:

bm_horse := new(Animal)
bm_horse := &Animal{}

使用哪种方式取决于自己。但如果要进行初始化赋值,一般采用第二种方法,可读性更强:

# 第一种方式
bm_horse := new(Animal)
bm_horse.name = "baima"
bm_horse.weight = 60 # 第二种方式
bm_horse := &Animal{
name: "baima",
weight: 60,
}

扩展数据结构的字段

在前面出现的数据结构中的字段数据类型都是简简单单的内置类型:string、int。但数据结构中的字段可以更复杂,例如可以是map、array等,还可以是自定义的数据类型(数据结构)。

例如,将一个指向同类型数据结构的字段添加到数据结构中:

type Animal struct {
name string
weight int
father *Animal
}

其中在此处的*Animal所表示的数据结构实例很可能是其它的Animal实例对象。

上面定义了father,还可以定义son,sister等等。

例如:

bm_horse := &Animal{
name: "baima",
weight: 60,
father: &Animal{
name: "hongma",
weight: 80,
father: nil,
},
}

composition

Go语言支持Composition(组合),它表示的是在一个数据结构中嵌套另一个数据结构的行为。

package main

import (
"fmt"
) type Animal struct {
name string
weight int
} type Horse struct {
*Animal // 注意此行
speak string
} func (a *Animal) hello() {
fmt.Println(a.name)
fmt.Println(a.weight)
//fmt.Println(a.speak)
} func main() {
bm_horse := &Horse{
Animal: &Animal{ // 注意此行
name: "baima",
weight: 60,
},
speak: "neigh",
}
bm_horse.hello()
}

上面的Horse数据结构中包含了一行*Animal,表示Animal的数据结构插入到Horse的结构中,这就像是一种面向对象的类继承。注意,没有给该字段显式命名,但可以隐式地访问Horse组合结构中的字段和函数。

另外,在构建Horse实例的时候,必须显式为其指定字段名(尽管数据结构中并没有指定其名称),且字段的名称必须和数据结构的名称完全相同。

然后调用属于Animal数据结构的hello方法,它只能访问Animal中的属性,所以无法访问speak属性。

很多人认为这种代码共享的方式比面向对象的继承更加健壮。

Go中的重载overload

例如,将上面属于Animal数据结构的hello函数重载为属于Horse数据结构的hello函数:

package main

import (
"fmt"
) type Animal struct {
name string
weight int
} type Horse struct {
*Animal // 注意此行
speak string
} func (h *Horse) hello() {
fmt.Println(h.name)
fmt.Println(h.weight)
fmt.Println(h.speak)
} func main() {
bm_horse := &Horse{
Animal: &Animal{ // 注意此行
name: "baima",
weight: 60,
},
speak: "neigh",
}
bm_horse.hello()
}

go基础系列:结构struct的更多相关文章

  1. Go基础系列:struct和嵌套struct

    struct struct定义结构,结构由字段(field)组成,每个field都有所属数据类型,在一个struct中,每个字段名都必须唯一. 说白了就是拿来存储数据的,只不过可自定义化的程度很高,用 ...

  2. Go基础系列:struct的导出和暴露问题

    struct的导出和暴露问题 关于struct的导出 struct的属性是否被导出,也遵循大小写的原则:首字母大写的被导出,首字母小写的不被导出. 所以: 如果struct名称首字母是小写的,这个st ...

  3. JVM基础系列第6讲:Java 虚拟机内存结构

    看到这里,我相信大家对于一个 Java 源文件是如何变成字节码文件,以及字节码文件的含义已经非常清楚了.那么接下来就是让 Java 虚拟机运行字节码文件,从而得出我们最终想要的结果了.在这个过程中,J ...

  4. 夯实Java基础系列5:Java文件和Java包结构

    目录 Java中的包概念 包的作用 package 的目录结构 设置 CLASSPATH 系统变量 常用jar包 java软件包的类型 dt.jar rt.jar *.java文件的奥秘 *.Java ...

  5. 基础系列(4)—— C#装箱和拆箱

    一 装箱和拆箱的概念 装箱是将值类型转换为引用类型 : 拆箱是将引用类型转换为值类型 : 值类型:包括原类型(Sbyte.Byte.Short.Ushort.Int.Uint.Long.Ulong.C ...

  6. 基础系列(5)—— C#控制语句

    语句是程序中最小程序指令.C#语言中可以使用多种类型的语句,每一种类型的语句又可以通过多个关键字实现.以下是C# 语言中使用的主要控制语句 类别 关键字 选择语句  if.else.switch.ca ...

  7. linux高级编程基础系列:线程间通信

    linux高级编程基础系列:线程间通信 转载:原文地址http://blog.163.com/jimking_2010/blog/static/1716015352013102510748824/ 线 ...

  8. 带你学够浪:Go语言基础系列 - 8分钟学复合类型

    ★ 文章每周持续更新,原创不易,「三连」让更多人看到是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) " 对于一般的语言使用者来说 ,20% ...

  9. 带你学够浪:Go语言基础系列 - 10分钟学方法和接口

    文章每周持续更新,原创不易,「三连」让更多人看到是对我最大的肯定.可以微信搜索公众号「 后端技术学堂 」第一时间阅读(一般比博客早更新一到两篇) 对于一般的语言使用者来说 ,20% 的语言特性就能够满 ...

  10. C#基础系列——小话泛型

    前言:前面两章介绍了C#的两个常用技术:C#基础系列——反射笔记 和 C#基础系列——Attribute特性使用 .这一章来总结下C#泛型技术的使用.据博主的使用经历,觉得泛型也是为了重用而生的,并且 ...

随机推荐

  1. 基于jquery的-获取短信验证码-倒计时

    在制作短信验证的时候,需要做一个获取短信按钮,点击后显示倒计时, html代码如下: <input class="gain" type="button" ...

  2. angularJS自定义属性作为条件中转

    <html> <head> <meta charset="utf-8"/> <title></title> </h ...

  3. Ubuntu安装提示Permission Denied

    我用wubi安装ubuntu 显示 permission denied 并要查看日志文件 怎么办啊? 你好,你把你的ISO放到你的Wubi目录下面,也就是把镜像放到你解压好的文件夹里面就可以了呢!! ...

  4. Javascript substr方法在某些浏览器下行为出现BUG的补丁代码

    主要思路是使用兼容性和稳定性都保持一致的substring方法重写/覆盖substr /** * String.substr() bug fix * @param start * @param len ...

  5. 安装Oracle10g on RedHat as 4 64bit(摘)

    一.安装前的配置 1.修改RH版本 vi /etc/redhat-release Red Hat Enterprise Linux AS release 3 (Taroon Update 3) 2. ...

  6. Tomcat数据源

    Tomcat数据源原理,配置,使用 在程序中,使用数据源是可以提升操作性能的,这种性能 的提升依靠运行的操作原理. 传统的JDBC操作: 1. 加载数据库驱动,通过CLASSPATH配置 2. 通过D ...

  7. 深入解析ThreadLocal 详解、实现原理、使用场景方法以及内存泄漏防范 多线程中篇(十七)

    简介 从名称看,ThreadLocal 也就是thread和local的组合,也就是一个thread有一个local的变量副本 ThreadLocal提供了线程的本地副本,也就是说每个线程将会拥有一个 ...

  8. python 历险记(四)— python 中常用的 json 操作

    目录 引言 基础知识 什么是 JSON? JSON 的语法 JSON 对象有哪些特点? JSON 数组有哪些特点? 什么是编码和解码? 常用的 json 操作有哪些? json 操作需要什么库? 如何 ...

  9. C&num; 实现数字字符串左补齐0的两种方法

    ); MessageBox.Show(sss); return; 代码如上,自动补齐前面的0

  10. 以DefaultFuture为例,看类变量初始化顺序

    https://*.com/questions/8517121/java-what-is-the-difference-between-init-and-clinit# &lt ...