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

智能推荐

AndroidStudio无代码高亮解决办法_android studio 高亮-程序员宅基地

文章浏览阅读2.8k次。AndroidStudio 升级到 4.2.2 版本后,没有代码高亮了,很蛋疼。解决办法是:点开上方的 File,先勾选 Power Save Mode 再取消就可以了。_android studio 高亮

swift4.0 valueForUndefinedKey:]: this class is not key value coding-compliant for the key unity.'_forundefinedkey swift4-程序员宅基地

文章浏览阅读1k次。使用swift4.0整合Unity出现[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key unity.'在对应属性前加@objc 即可。或者调回swift3.2版本_forundefinedkey swift4

Spring Security2的COOKIE的保存时间设置_springsecurity 设置cookie失效时间-程序员宅基地

文章浏览阅读1.3k次。http auto-config="true" access-denied-page="/common/403.htm"> intercept-url pattern="/login.**" access="IS_AUTHENTICATED_ANONYMOUSLY"/> form-login login-page="/login.jsp" defau_springsecurity 设置cookie失效时间

view滑动冲突解决实战篇2(外部拦截法)_viewpage2外部拦截事件-程序员宅基地

文章浏览阅读1.1k次。继上篇内部拦截法需求还是跟上篇一样。只不过这次用外部拦截法来解决;只要在父容器添加如下代码就可以解决了滑动冲突,很简单,套模板就行 // 分别记录上次滑动的坐标(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; @Override public bo_viewpage2外部拦截事件

汇编 堆栈 变量存储 指针_汇编语言栈指针-程序员宅基地

文章浏览阅读2.5k次,点赞7次,收藏9次。本文章系作者原创,未经许可,不得转载。汇编 堆栈 变量存储 指针先说栈的概念,栈其实也是一种。。。。。先说内存的概念吧。。。。。额 先说计算机吧,简单来说的话,可以把计算机理解成由CPU,内存,硬盘组成,而CPU内部又包括一种叫做内部寄存器的东西,包括 数据寄存器: AX,BX,CX,DX; 段寄存器: CS,DS,ES,SS; 指针与变址寄存器SP,BP,SI,DI; ..._汇编语言栈指针

架构师之路:从码农到架构师你差了哪些_web架构师-程序员宅基地

文章浏览阅读1w次,点赞14次,收藏56次。转载自 架构师之路:从码农到架构师你差了哪些 Web应用,最常见的研发语言是Java和PHP。 后端服务,最常见的研发语言是Java和C/C++。 大数据,最常见的研发语言是Java和Python。 可以说,Java是现阶段中国互联网公司中,覆盖度最广的研发语言,掌握了Java技术体系,不管在成熟的大公司,快速发展的公司,还是创业阶段的公司,都能有立足之地。有..._web架构师

随便推点

超级简单的Python爬虫入门教程(非常详细),通俗易懂,看一遍就会了_爬虫python入门-程序员宅基地

文章浏览阅读7.3k次,点赞6次,收藏36次。超级简单的Python爬虫入门教程(非常详细),通俗易懂,看一遍就会了_爬虫python入门

python怎么输出logistic回归系数_python - Logistic回归scikit学习系数与统计模型的系数 - SO中文参考 - www.soinside.com...-程序员宅基地

文章浏览阅读1.2k次。您的代码存在一些问题。首先,您在此处显示的两个模型是not等效的:尽管您将scikit-learn LogisticRegression设置为fit_intercept=True(这是默认设置),但您并没有这样做statsmodels一;来自statsmodels docs:默认情况下不包括拦截器,用户应添加。参见statsmodels.tools.add_constant。另一个问题是,尽管您处..._sm fit(method

VS2017、VS2019配置SFML_vsllfqm-程序员宅基地

文章浏览阅读518次。一、sfml官网下载32位的版本 一样的设置,64位的版本我没有成功,用不了。二、三、四以下这些内容拷贝过去:sfml-graphics-d.libsfml-window-d.libsfml-system-d.libsfml-audio-d.lib..._vsllfqm

vc——类似与beyondcompare工具的文本比较算法源代码_byoned compare 字符串比较算法-程序员宅基地

文章浏览阅读2.7k次。由于工作需要,要做一个类似bc2的文本比较工具,用红色字体标明不同的地方,研究了半天,自己写了一个简易版的。文本比较的规则是1.先比较文本的行数,2.再比较对应行的字符串的长度3.再比较每一个字符串是否相同。具体代码如下:其中m_basestr和m_mergestr里面存放是待比较的字符串int basecount=m_basestr.GetLength(); int mergec_byoned compare 字符串比较算法

aetna java_pom.xml-程序员宅基地

文章浏览阅读79次。xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">org.apacheapache174.0.0org.apache.atlasapache-atlas3.0.0-SNAPSHOTMetadata Management and Data Govern..._atlas.pom

生成随机数_<math.h>随机数-程序员宅基地

文章浏览阅读1.5k次。C语言中有可以产生随机数据的函数,需要添加 stdlib. h头文件与time.h头文件。首先在main函数开头加上“ srand(unsigned)time(NULL));",这个语句将生成随机数的种子(不懂也没关系,只要记住这个语句,并且知道 srand是初始化随机种子用的即可)。然后,在需要使用随机数的地方使用 rand()函数。下面是一段生成十个随机数的代码:程序代码:#incl..._随机数