《快学 Go 语言》第 8 课 —— 程序大厦是如何构建起来的

2023-11-28 01:48

本文主要是介绍《快学 Go 语言》第 8 课 —— 程序大厦是如何构建起来的,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

640?wx_fmt=jpeg

本节我们要开讲 Go 语言在数据结构上最重要的概念 —— 结构体。如果说 Go 语言的基础类型是原子,那么结构体就是分子。分子是原子的组合,让形式有限的基础类型变化出丰富多样的形态结构。结构体里面装的是基础类型、切片、字典、数组以及其它类型的结构体等等。

640?wx_fmt=png

因为结构体的存在,Go 语言的变量才有了更加丰富多彩的形式,Go 语言程序的高楼大厦正是通过结构体一层层组装起来的。

结构体类型的定义

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

type Circle struct {x inty intRadius int
}

结构体变量的创建

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

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

package mainimport "fmt"type Circle struct {x inty intRadius 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 mainimport "fmt"type Circle struct {x inty intRadius int
}func main() {var c Circle = Circle {100, 100, 50}fmt.Printf("%+v\n", c)
}-------
{x:100 y:100 Radius:50}

package mainimport "fmt"type Circle struct {x inty intRadius int
}func main() {var c *Circle = &Circle {100, 100, 50}fmt.Printf("%+v\n", c)
}-----------
&{x:100 y:100 Radius:50}

package mainimport "fmt"type Circle struct {x inty intRadius int
}func main() {var c *Circle = new(Circle)fmt.Printf("%+v\n", c)
}----------
&{x:0 y:0 Radius:0}

package mainimport "fmt"type Circle struct {x inty intRadius int
}func main() {var c Circlefmt.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 mainimport "fmt"
import "unsafe"type Circle struct {x inty intRadius int
}func main() {var c Circle = Circle {Radius: 50}fmt.Println(unsafe.Sizeof(c))
}-------
24

结构体的拷贝

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

package mainimport "fmt"type Circle struct {x inty intRadius int
}func main() {var c1 Circle = Circle {Radius: 50}var c2 Circle = c1fmt.Printf("%+v\n", c1)fmt.Printf("%+v\n", c2)c1.Radius = 100fmt.Printf("%+v\n", c1)fmt.Printf("%+v\n", c2)var c3 *Circle = &Circle {Radius: 50}var c4 *Circle = c3fmt.Printf("%+v\n", c3)fmt.Printf("%+v\n", c4)c3.Radius = 100fmt.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:50}
&{x:0 y:0 Radius:50}

无处不在的结构体

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

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

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

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

type hmap struct {count int...buckets unsafe.Pointer  // hash桶地址...
}

结构体中的数组和切片

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

package mainimport "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 mainimport "fmt"type Circle struct {x inty intRadius 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 mainimport "fmt"
import "math"type Circle struct {x inty intRadius 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 = &cfmt.Println(pc.Area(), pc.Circumference())
}-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793

结构体的指针方法

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

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

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

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

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

内嵌结构体

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

package mainimport "fmt"type Point struct {x inty int
}func (p Point) show() {fmt.Println(p.x, p.y)
}type Circle struct {loc PointRadius 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 mainimport "fmt"type Point struct {x inty 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

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

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

package mainimport "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

面向对象的多态性需要通过 Go 语言的接口特性来模拟,这就是下一节我们要讲的主题。

这篇关于《快学 Go 语言》第 8 课 —— 程序大厦是如何构建起来的的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/428871

相关文章

C语言中位操作的实际应用举例

《C语言中位操作的实际应用举例》:本文主要介绍C语言中位操作的实际应用,总结了位操作的使用场景,并指出了需要注意的问题,如可读性、平台依赖性和溢出风险,文中通过代码介绍的非常详细,需要的朋友可以参... 目录1. 嵌入式系统与硬件寄存器操作2. 网络协议解析3. 图像处理与颜色编码4. 高效处理布尔标志集合

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

C 语言中enum枚举的定义和使用小结

《C语言中enum枚举的定义和使用小结》在C语言里,enum(枚举)是一种用户自定义的数据类型,它能够让你创建一组具名的整数常量,下面我会从定义、使用、特性等方面详细介绍enum,感兴趣的朋友一起看... 目录1、引言2、基本定义3、定义枚举变量4、自定义枚举常量的值5、枚举与switch语句结合使用6、枚

将Java程序打包成EXE文件的实现方式

《将Java程序打包成EXE文件的实现方式》:本文主要介绍将Java程序打包成EXE文件的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录如何将Java程序编程打包成EXE文件1.准备Java程序2.生成JAR包3.选择并安装打包工具4.配置Launch4

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

使用Python构建一个Hexo博客发布工具

《使用Python构建一个Hexo博客发布工具》虽然Hexo的命令行工具非常强大,但对于日常的博客撰写和发布过程,我总觉得缺少一个直观的图形界面来简化操作,下面我们就来看看如何使用Python构建一个... 目录引言Hexo博客系统简介设计需求技术选择代码实现主框架界面设计核心功能实现1. 发布文章2. 加

go 指针接收者和值接收者的区别小结

《go指针接收者和值接收者的区别小结》在Go语言中,值接收者和指针接收者是方法定义中的两种接收者类型,本文主要介绍了go指针接收者和值接收者的区别小结,文中通过示例代码介绍的非常详细,需要的朋友们下... 目录go 指针接收者和值接收者的区别易错点辨析go 指针接收者和值接收者的区别指针接收者和值接收者的

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

SpringBoot实现微信小程序支付功能

《SpringBoot实现微信小程序支付功能》小程序支付功能已成为众多应用的核心需求之一,本文主要介绍了SpringBoot实现微信小程序支付功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作... 目录一、引言二、准备工作(一)微信支付商户平台配置(二)Spring Boot项目搭建(三)配置文件

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印