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

智能推荐

列式存储-程序员宅基地

文章浏览阅读1.1k次。OLAP中数据存储的问题OLAP 需要队列进行选择,行式存储按行存数据,使用索引加快对数据的查找(索引包括聚集索引(表记录的排列顺序与索引的排列顺序一致)和非聚簇索引(非聚集索引指定了表中记录的逻辑顺序,但记录的物理顺序和索引的顺序不一致))。这种方式对按列的存储和检索不是很高效,查询某一列数据需要将所有行的数据扫描一次,而且对统计分析也不友好。列式存储原理若使用列式存储可以只用扫描出需要的列,行、列存储的对比。文件格式parquet 文件格式:如下图所示:parquet file = hea_列式存储

C语言字符串详解-程序员宅基地

文章浏览阅读4.3w次,点赞184次,收藏1.2k次。我们可以把字符串储存在char类型的数组中,如果char类型的数组末尾包含一个表示字符串末尾的空字符\0,则该数组中的内容就构成了一个字符串因为字符串需要用\0结尾,所以在定义字符串的时候,字符数组的长度要预留多一个字节用来存放\0,\0就是数字0例如。_c语言字符串

vue3常用自定义指令封装v-permission,按钮权限控制,添加防抖节流_vue3 v-permission-程序员宅基地

文章浏览阅读2k次,点赞8次,收藏15次。后台管理项目免不了要做权限控制,常见的分为路由级别和按钮级别,在此主要针对于按钮权限,比如说某个用户或者角色对数据有没有增删改查的权限,例如以下功能,巡查人员可以点击导入和新建,而一般用户只能选择下载模板。在 directives文件夹下分别创建permission、debounce、throttle三个ts文件,分别用于存放权限控制,防抖和节流的业务逻辑,结构清晰,方便维护以及更低的耦合度。在index.ts文件中分别导入每个自定义指令对象,再遍历注册每一个指令。_vue3 v-permission

maven-dependency-versions-check-plugin, Maven 插件查找依赖版本冲突-程序员宅基地

文章浏览阅读553次。1、maven-dependency-versions-check-plugin, Maven 插件查找依赖版本冲突转载于:https://www.cnblogs.com/yixiu868/p/11583582.html_maven-dependency-versions-check-plugin

easyUI中datagrid单选时复选框不取消问题_easyui datagrid 设置singleselect:true 后不能取消选择-程序员宅基地

文章浏览阅读472次。圈起来的地方是需要注意的这里的代码可以多选,也可以把多选的复选框全部取消勾选如果singleSelect改为true,就不能取消勾选,至于为什么,我还没弄懂(笑哭)_easyui datagrid 设置singleselect:true 后不能取消选择

TinoyOs和nesC语言-程序员宅基地

文章浏览阅读479次。nesC语言编程基础1.接口NesC程序主要由各式组件(component)构成,组件和组件之间通过特定的接口(interface)互相沟通。一个接口内声明了提供相关服务的方法(C语言函数)。例如数据读取接口(Read)内就包含了读取(read)、读取结束(readDone)函数。接口只是制定了组件之间交流的规范,也就是通过某一个接口,只能通过该接口提供的方法实现两个组件之间的交流。但是接口终归只是接口,只是一组函数的声明,并为包含对接口的实现。1 interface Read<val._nesc语言

随便推点

前后端分离之Spring Security Api验证实践-程序员宅基地

文章浏览阅读1.3k次。前后端分离之Spring Security Api验证实践为什么需要RESTful重定向问题为什么需要RESTful使用RESTful之前,会发现各种奇葩的url命名,对url的功能经常需要结合源代码来确认,让人头痛,使用RESTful规范之后,很多问题得以解决。仅仅依靠URL和Method就能定为功能。重定向问题需要重新定义逻辑(JDK8推荐使用Lambda表达式)登录 ,默认下..._spring security api

图像处理之常见二值化方法汇总-程序员宅基地

文章浏览阅读10w+次,点赞25次,收藏117次。图像处理之常见二值化方法汇总图像二值化是图像分析与处理中最常见最重要的处理手段,二值处理方法也非常多。越精准的方法计算量也越大。本文主要介绍四种常见的二值处理方法,通常情况下可以满足大多数图像处理的需要。主要本文讨论的方法仅针对RGB色彩空间。 方法一:该方法非常简单,对RGB彩色图像灰度化以后,扫描图像的每个像素值,值小于127的将像素值设为0(黑色),值大于等于12_二值化

GUI程序开发_gui开发-程序员宅基地

文章浏览阅读1.9k次。JAVA程序设计与应用开发(第2版)——《GUI清华大学出版社》_gui开发

PYTHON实训总结及体会500字,PYTHON实训总结思考建议_python实验体会-程序员宅基地

文章浏览阅读491次。大家好,给大家分享一下PYTHON实训总结及体会1500字,很多人还不知道这一点。这将使你在做实验时的难度加大。然后两下子就将实验报告做完。但学到的知识与难度成正比。一定要将课本上的知识吃透。【篇一:实验心得体会】就像以前做物理实验一样。在老师讲解时就会听不懂。你要清楚电桥的各种接法。这将使你极大地浪费时间。在做测试技术的实验前。因为这是做实验的基础。_python实验体会

ADC参数详解_adc电流电压零漂值-程序员宅基地

文章浏览阅读9.6k次,点赞9次,收藏117次。特性或指标总述本文将从以下特性进行简单的叙述。结合了《ADC设计基础》和TI的一些教学视频。分辨率转换误差转换速度采样率奈奎斯特采样准则混叠和抗混叠滤波器DNLINL热噪声谐波失真THDSNRENOBSFDRIMD孔径抖动孔径延迟奈奎斯特区补充分辨率一般ADC都说注明是8bit,16bit或者是24bit。这里的数值也就是分辨率的意思。分辨率是衡量A..._adc电流电压零漂值

服务器阵列卡缓存显示错误,服务器阵列卡(缓存)-程序员宅基地

文章浏览阅读1.1k次。RAID卡介绍:提到RAID卡就不得不提到什么是RAID。RAID是英文Redundant Array of Independent Disks的缩写,翻译成中文即为独立磁盘冗余阵列,或简称磁盘阵列。简单的说,RAID是一种把多块独立的硬盘(物理硬盘)按不同方式组合起来形成一个硬盘组(逻辑硬盘),从而提供比单个硬盘更高的存储性能和提供数据冗余的技术。组成磁盘阵列的不同方式成为RAID级别(RAID..._把raid缓存强制开启 显示参数无效

推荐文章

热门文章

相关标签