Go语言struct——与众不同的结构体_go 常量 struct-程序员宅基地

技术标签: go基础  golang  go  

前言

物以类聚,人以群分。结构体就是go语言里面典型的代表,每个结构体都有自己不同的成员

结构体类型的定义

结构体和其它语言里的"类"比较类似。下面我们使用结构体语法来定义一个"圆"

type Circle struct {
    
  x int
  y int
  Radius int
}

Circle 结构体内部有三个变量,分别是圆心的坐标以及半径。特别需要注意是结构体内部变量的大小写,首字母大写是公开变量,首字母小写是内部变量,分别相当于类成员变量的 Public 和 Private 类别。内部变量只有属于同一个 package(简单理解就是同一个目录)的代码才能直接访问。

结构体变量的创建

创建一个结构体变量有多种形式,我们先看结构体变量最常见的创建形式

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c Circle = Circle {
    
        x: 100,
        y: 100,
        Radius: 50,  // 注意这里的逗号不能少
    }
    fmt.Printf("%+v\n", c)
}

----------
{
    x:100 y:100 Radius:50}

通过显示指定结构体内部字段的名称和初始值来初始化结构体,可以只指定部分字段的初值,甚至可以一个字段都不指定,那些没有指定初值的字段会自动初始化为相应类型的「零值」。这种形式我们称之为 「KV 形式」。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c1 Circle = Circle {
    
        Radius: 50,
    }
    var c2 Circle = Circle {
    }
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
}

----------
{
    x:0 y:0 Radius:50}
{
    x:0 y:0 Radius:0}

结构体的第二种创建形式是不指定字段名称来顺序字段初始化,需要显示提供所有字段的初值,一个都不能少。这种形式称之为「顺序形式」。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c Circle = Circle {
    100, 100, 50}
    fmt.Printf("%+v\n", c)
}

-------
{
    x:100 y:100 Radius:50}

结构体变量和普通变量都有指针形式,使用取地址符就可以得到结构体的指针类型

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c *Circle = &Circle {
    100, 100, 50}
    fmt.Printf("%+v\n", c)
}

-----------
&{
    x:100 y:100 Radius:50}

注意上面的输出,指针形式多了一个地址符 &,表示打印的对象是一个指针类型。介绍完了结构体变量的指针形式,下面就可以引入结构体变量创建的第三种形式,使用全局的 new() 函数来创建一个「零值」结构体,所有的字段都被初始化为相应类型的零值。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c *Circle = new(Circle)
    fmt.Printf("%+v\n", c)
}

----------
&{
    x:0 y:0 Radius:0}

注意 new() 函数返回的是指针类型。下面再引入结构体变量的第四种创建形式,这种形式也是零值初始化,就数它看起来最不雅观。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c Circle
    fmt.Printf("%+v\n", c)
}

最后将三种零值初始化形式放到一起对比观察一下

var c1 Circle = Circle{
    }
var c2 Circle
var c3 *Circle = new(Circle)
零值结构体和 nil 结构体

nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。

var c *Circle = nil

而零值结构体是会实实在在占用内存空间的,只不过每个字段都是零值。如果结构体里面字段非常多,那么这个内存空间占用肯定也会很大。

结构体的内存大小

Go 语言的 unsafe 包提供了获取结构体内存占用的函数 Sizeof()

package main

import "fmt"
import "unsafe"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c Circle = Circle {
    Radius: 50}
    fmt.Println(unsafe.Sizeof(c))
}

-------
24

Circle 结构体在我的 64位机器上占用了 24 个字节,因为每个 int 类型都是 8 字节。在 32 位机器上,Circle 结构体只会占用 12 个字节。

结构体的拷贝

结构体之间可以相互赋值,它在本质上是一次浅拷贝操作,拷贝了结构体内部的所有字段。结构体指针之间也可以相互赋值,它在本质上也是一次浅拷贝操作,不过它拷贝的仅仅是指针地址值,结构体的内容是共享的。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func main() {
    
    var c1 Circle = Circle {
    Radius: 50}
    var c2 Circle = c1
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
    c1.Radius = 100
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)

    var c3 *Circle = &Circle {
    Radius: 50}
    var c4 *Circle = c3
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
    c3.Radius = 100
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
}

---------------
{
    x:0 y:0 Radius:50}
{
    x:0 y:0 Radius:50}
{
    x:0 y:0 Radius:100}
{
    x:0 y:0 Radius:50}
&{
    x:0 y:0 Radius:50}
&{
    x:0 y:0 Radius:50}
&{
    x:0 y:0 Radius:100}
&{
    x:0 y:0 Radius:100}
无处不在的结构体

通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。

切片头的结构体形式如下,它在 64 位机器上将会占用 24 个字节

type slice struct {
    
  array unsafe.Pointer  // 底层数组的地址
  len int // 长度
  cap int // 容量
}

字符串头的结构体形式,它在 64 位机器上将会占用 16 个字节

type string struct {
    
  array unsafe.Pointer // 底层数组的地址
  len int
}

字典头的结构体形式

type hmap struct {
    
  count int
  ...
  buckets unsafe.Pointer  // hash桶地址
  ...
}
结构体中的数组和切片

在数组与切片章节,我们自习分析了数组与切片在内存形式上的区别。数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。请读者尝试解释一下下面代码的输出结果

package main

import "fmt"
import "unsafe"

type ArrayStruct struct {
    
    value [10]int
}

type SliceStruct struct {
    
    value []int
}

func main() {
    
    var as = ArrayStruct{
    [...]int{
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
    var ss = SliceStruct{
    []int{
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
    fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}

-------------
80 24

注意代码中的数组初始化使用了 […] 语法糖,表示让编译器自动推导数组的长度。

结构体的参数传递

函数调用时参数传递结构体变量,Go 语言支持值传递,也支持指针传递。值传递涉及到结构体字段的浅拷贝,指针传递会共享结构体内容,只会拷贝指针地址,规则上和赋值是等价的。下面我们使用两种传参方式来编写扩大圆半径的函数。

package main

import "fmt"

type Circle struct {
    
    x int
    y int
    Radius int
}

func expandByValue(c Circle) {
    
    c.Radius *= 2
}

func expandByPointer(c *Circle) {
    
    c.Radius *= 2
}

func main() {
    
    var c = Circle {
    Radius: 50}
    expandByValue(c)
    fmt.Println(c)
    expandByPointer(&c)
    fmt.Println(c)
}

---------
{
    0 0 50}
{
    0 0 100}

从上面的输出中可以看到通过值传递,在函数里面修改结构体的状态不会影响到原有结构体的状态,函数内部的逻辑并没有产生任何效果。通过指针传递就不一样。

结构体方法

Go 语言不是面向对象的语言,它里面不存在类的概念,结构体正是类的替代品。类可以附加很多成员方法,结构体也可以。

package main

import "fmt"
import "math"

type Circle struct {
    
 x int
 y int
 Radius int
}

// 面积
func (c Circle) Area() float64 {
    
 return math.Pi * float64(c.Radius) * float64(c.Radius)
}

// 周长
func (c Circle) Circumference() float64 {
    
 return 2 * math.Pi * float64(c.Radius)
}

func main() {
    
 var c = Circle {
    Radius: 50}
 fmt.Println(c.Area(), c.Circumference())
 // 指针变量调用方法形式上是一样的
 var pc = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793

Go 语言不喜欢类型的隐式转换,所以需要将整形显示转换成浮点型,不是很好看,不过这就是 Go 语言的基本规则,显式的代码可能不够简洁,但是易于理解。

Go 语言的结构体方法里面没有 self 和 this 这样的关键字来指代当前的对象,它是用户自己定义的变量名称,通常我们都使用单个字母来表示。
Go 语言的方法名称也分首字母大小写,它的权限规则和字段一样,首字母大写就是公开方法,首字母小写就是内部方法,只能归属于同一个包的代码才可以访问内部方法。

结构体的值类型和指针类型访问内部字段和方法在形式上是一样的。这点不同于 C++ 语言,在 C++ 语言里,值访问使用句点 . 操作符,而指针访问需要使用箭头 -> 操作符。

结构体的指针方法

如果使用上面的方法形式给 Circle 增加一个扩大半径的方法,你会发现半径扩大不了。

func (c Circle) expand() {
    
  c.Radius *= 2
}

这是因为上面的方法和前面的 expandByValue 函数是等价的,只不过是把函数的第一个参数挪了位置而已,参数传递时会复制了一份结构体内容,起不到扩大半径的效果。这时候就必须要使用结构体的指针方法

func (c *Circle) expand() {
    
  c.Radius *= 2
}

结构体指针方法和值方法在调用时形式上是没有区别的,只不过一个可以改变结构体内部状态,而另一个不会。指针方法使用结构体值变量可以调用,值方法使用结构体指针变量也可以调用。

通过指针访问内部的字段需要 2 次内存读取操作,第一步是取得指针地址,第二部是读取地址的内容,它比值访问要慢。但是在方法调用时,指针传递可以避免结构体的拷贝操作,结构体比较大时,这种性能的差距就会比较明显。

还有一些特殊的结构体它不允许被复制,比如结构体内部包含有锁时,这时就必须使用它的指针形式来定义方法,否则会发生一些莫名其妙的问题。

内嵌结构体

结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 Go 语言里称之为「组合」。下面我们来看看内嵌结构体的基本使用方法

package main

import "fmt"

type Point struct {
    
    x int
    y int
}

func (p Point) show() {
    
  fmt.Println(p.x, p.y)
}

type Circle struct {
    
    loc Point
    Radius int
}

func main() {
    
    var c = Circle {
    
        loc: Point {
    
            x: 100,
            y: 100,
        },
        Radius: 50,
    }
    fmt.Printf("%+v\n", c)
    fmt.Printf("%+v\n", c.loc)
    fmt.Printf("%d %d\n", c.loc.x, c.loc.y)
 c.loc.show()
}

----------------
{
    loc:{
    x:100 y:100} Radius:50}
{
    x:100 y:100}
100 100
100 100
匿名内嵌结构体

还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,就好像把子结构体的一切全部都揉进了父结构体一样。匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称

package main

import "fmt"

type Point struct {
    
    x int
    y int
}

func (p Point) show() {
    
    fmt.Println(p.x, p.y)
}

type Circle struct {
    
    Point // 匿名内嵌结构体
    Radius int
}

func main() {
    
    var c = Circle {
    
        Point: Point {
    
            x: 100,
            y: 100,
        },
        Radius: 50,
    }
    fmt.Printf("%+v\n", c)
    fmt.Printf("%+v\n", c.Point)
    fmt.Printf("%d %d\n", c.x, c.y) // 继承了字段
    fmt.Printf("%d %d\n", c.Point.x, c.Point.y)
 c.show() // 继承了方法
 c.Point.show()
}

-------
{
    Point:{
    x:100 y:100} Radius:50}
{
    x:100 y:100}
100 100
100 100
100 100
100 100

这里的继承仅仅是形式上的语法糖,c.show() 被转换成二进制代码后和 c.Point.show() 是等价的,c.x 和 c.Point.x 也是等价的。

Go 语言的结构体没有多态性

Go 语言不是面向对象语言在于它的结构体不支持多态,它不能算是一个严格的面向对象语言。多态是指父类定义的方法可以调用子类实现的方法,不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。下面的例子呈现了 Java 类的多态性。

class Fruit {
    
  public void eat() {
    
    System.out.println("eat fruit");
  }

  public void enjoy() {
    
    System.out.println("smell first");
    eat();
    System.out.println("clean finally");
  }
}

class Apple extends Fruit {
    
  public void eat() {
    
    System.out.println("eat apple");
  }
}

class Banana extends Fruit {
    
  public void eat() {
    
    System.out.println("eat banana");
  }
}

public class Main {
    
  public static void main(String[] args) {
    
    Apple apple = new Apple();
    Banana banana = new Banana();
    apple.enjoy();
    banana.enjoy();
  }
}

----------------
smell first
eat apple
clean finally
smell first
eat banana
clean finally

父类 Fruit 定义的 enjoy 方法调用了子类实现的 eat 方法,子类的方法可以对父类定义的方法进行覆盖,父类的 eat 方法被隐藏起来了。

Go 语言的结构体明确不支持这种形式的多态,外结构体的方法不能覆盖内部结构体的方法。比如我们用 Go 语言来改写上面的水果例子观察一下输出结果。

package main

import "fmt"

type Fruit struct {
    }

func (f Fruit) eat() {
    
    fmt.Println("eat fruit")
}

func (f Fruit) enjoy() {
    
    fmt.Println("smell first")
    f.eat()
    fmt.Println("clean finally")
}

type Apple struct {
    
    Fruit
}

func (a Apple) eat() {
    
    fmt.Println("eat apple")
}

type Banana struct {
    
    Fruit
}

func (b Banana) eat() {
    
    fmt.Println("eat banana")
}

func main() {
    
    var apple = Apple {
    }
    var banana = Banana {
    }
    apple.enjoy()
    banana.enjoy()
}

----------
smell first
eat fruit
clean finally
smell first
eat fruit
clean finally
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43844941/article/details/107232720

智能推荐

日志_log4j:自定义log4j日志文件存放路径_wildfly部署后log4j对应的log在哪里-程序员宅基地

文章浏览阅读1.7k次。话说这是第一次自己配置log4j,以前都是拿来就用,完全没想过这个问题。 log4j的配置文件还是比较好用的,只要编译后log4j.properties文件是在classes目录下,它就会自动查找这个文件。 把记录日志放到文件中也是比较好配置的,问题是这个日志文件在哪儿,死活没找到?这让我有些慌张,如果不自己定义路径,将它控制住,就会觉得有些不舒服。 在网上搜了大半天,关于log4j讲解的文章_wildfly部署后log4j对应的log在哪里

JRebel热部署 xml文件修改 sql文件修改 不生效_jrebel 不更新sql-程序员宅基地

文章浏览阅读3.2k次。JRebel热部署 xml文件修改 sql文件修改 不生效_jrebel 不更新sql

论文学习笔记 POSEIDON: Privacy-Preserving Federated Neural Network Learning_poseidon: privacypreserving federated neural netwo-程序员宅基地

文章浏览阅读1.6k次,点赞6次,收藏14次。论文学习笔记 POSEIDON: Privacy-Preserving Federated Neural Network LearningNDSS 2021录用文章目录论文学习笔记 POSEIDON: Privacy-Preserving Federated Neural Network Learning一、机器学习1. 机器学习(ML)中的挑战2. 隐私保护机器学习(PPML)二、POSEIDON方案1. 系统和威胁模型2. 方案总览多方同态加密(MHE)联邦学习主要挑战和解决方法3. 方案CKKS_poseidon: privacypreserving federated neural network learning

opentsdb远程代码执行(CVE-2020-35476)-程序员宅基地

文章浏览阅读1.9k次。1漏洞背景OpenTSDB(Open Time Series Data Base)是基于HBASE构建的分布式、可扩展的时间序列数据库。OpenTSDB可以获取电力行业、化工行业、物联网行业等各类型实时监测、检查与分析设备所采集、产生的时间序列数据,并提供存储、索引以及图形化服务,使其易于访问和可视化。2 漏洞原理OpenTSDB 2.4.0及之前版本中存在远程代码执行漏洞,用户提交的yrange参数或其他相关参数的值在/src/tsd/GraphHandler.java文件中进行简单的反引号._cve-2020-35476

警惕rapidxml的陷阱(二):在Android上默认内存池分配数组过大,容易导致栈溢出_an element node. name contains element name. value-程序员宅基地

文章浏览阅读995次。项目中我们的模块很快写好了,在windows和linux上测试都工作的很好,但在Android上有时候却会崩溃。背景:我们的模块是c++写的,编译成so动态库在不同的平台(linux,windows,Android)上运行;Android上我们包装了一个service,通过jni加载so动态库运行的。 解决程序崩溃问题,首先要找到崩溃点。但我们的程序是service+jni的形式,直接_an element node. name contains element name. value contains text of first da

6.4.3 Xacro_完整使用流程示例_ros6.4-程序员宅基地

文章浏览阅读628次,点赞3次,收藏4次。ROS入门 6.4.3 Xacro_完整使用流程示例《ROS入门-理论与实践》视频教程镇楼》需求描述:使用 Xacro 优化 URDF 版的小车底盘模型实现结果演示:1.编写 Xacro 文件<!-- 使用 xacro 优化 URDF 版的小车底盘实现: 实现思路: 1.将一些常量、变量封装为 xacro:property 比如:PI 值、小车底盘半径、离地间距、车轮半径、宽度 .... 2.使用 宏 封装驱动轮以及支撑轮实现,调用相关_ros6.4

随便推点

Flask-RESTful 安装_flask_restful install-程序员宅基地

文章浏览阅读1.8k次。使用pip命令安装Flask-RESTfulpip install flask-restful可以从GitHub上的页面下载开发版本。git clone https://github.com/flask-restful/flask-restful.gitcd flask-restfulpython setup.py developFlask-RESTful具有以下依赖项(如果使用pip将自动安装):Flask版本0.10或更高Flask-RESTful需要Python版本2.7、3._flask_restful install

oozie4.3.1编译_oozie 4.3.1 编译-程序员宅基地

文章浏览阅读707次。oozie编译一、版本oozie版本:4.3.1JDK:1.8HADOOP:2.7.7hive:hivepig:0.16.0sqoop:1.4.7spark:2.3.0hbase:0.94.27tomcat:8.0.53二、编译前需要修改的地方root pom.xmljdk版本修改 &amp;amp;amp;amp;lt;properties&amp;amp;amp;amp;gt; &amp;amp;amp;amp;lt;targe_oozie 4.3.1 编译

2022-我的秋招之旅_合川电信面试-程序员宅基地

文章浏览阅读966次,点赞2次,收藏2次。123_合川电信面试

K-means原理、优化及应用-程序员宅基地

文章浏览阅读9.6w次,点赞57次,收藏392次。 K-Means算法是无监督的聚类算法,它实现起来比较简单,聚类效果也不错,因此应用很广泛。K-Means算法有大量的变体,本文就从最传统的K-Means算法讲起,在其基础上讲述K-Means的优化变体方法。包括初始化优化K-Means++, 距离计算优化elkan K-Means算法和大数据情况下的优化Mini Batch K-Means算法。1. K-Means原理初探    K-Me..._k-means

QCC305x-QCC304x系列开发教程(系统篇)之3.2-音频子系统详解_qcc 音频子系统开发-程序员宅基地

文章浏览阅读2.3k次。音频子系统功能:CPU时钟选项(灵活的时钟选项从2 MHz到120 MHz音频处理) 程序ROM: 6Mb 程序RAM/缓存:112KB 数据RAM大小:448KB 模拟DAC:单模模拟输出可配置为差分ab类音频输出或差分高效率输出 模拟ADC:立体声模拟输入可配置为单端线输入,或不平衡或平衡模拟麦克风输入 I2s/脉冲编码调制(PCM)接口1 x单向24位i2s接口 支持8kHz,16kHz,32kHz,44.1 kHz,48kHz,96kHz,192kHz,384kHz采样速..._qcc 音频子系统开发

ArcGIS API for JavaScript——地图展示分享-程序员宅基地

文章浏览阅读363次,点赞8次,收藏9次。https://zhuanlan.zhihu.com/p/93662287