Go语言与数据库开发:01-04

讲完基础数据类型之后,我们接着学习复合数据类型。

复合数据类型,它是以不同的方式组合基本类型可以构造出来的复合数据类型。


几个基本的复合数据类型:

数组,是由同构的元素组成——每个数组元素都是完全相同的类型;
结构体,则是由异构的元素组成的;

数组和结构体都是有固定内存大小的数据结构;

slice和map则是动态的数据结构,它们将根据需要动态增长;


数组:

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

因为数组的长度是固定的,因此在Go语言中很少直接使用数组。和数组对应的类型是
Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活,但是要理解slice工作原
理的话需要先理解数组。

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位
置。内置的len函数将返回数组中元素的个数。

var a[3] int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]

// Print the indices and elements.
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// Print the elements only.
for _, v := range a {
fmt.Printf("%d\n", v)
}

默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始
化值的个数来计算。因此,上面q数组的定义可以简化为
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我
们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候
数组才是相等的。不相等比较运算符!=遵循同样的规则。

当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数
参数变量接收的是一个复制的副本,并不是原始调用的变量。

因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改
都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。在这个方面,Go语言对
待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针
对象传入被调用的函数。

当然,我们可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以
直接反馈到调用者。

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依
然是僵化的类型,因为数组的类型包含了僵化的长度信息。

数组依然很少用作函数参数;相反,我们一般使用slice来替代数组。


slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作
[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序
列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分
构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址,要注意的
是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不
能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分
别返回slice的长度和容量。

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了
slice,因为新slice的长度会变大。

x[m:n]切片操作对于字符串则生成一个新字符串,如果x是[]byte的话则生成一个新的[]byte。

因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底
层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。

一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都
是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。

我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情
况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引
用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice
只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增
长用的。

内置的append函数用于向slice追加元素:
var runes []rune
for _, r := range "Hello, 世界" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"

为了提高内存使用效率,新分配的数组一般略大于保存x和y所需要的最低大小。通过在每次
扩展数组时直接将长度翻倍从而避免了多次内存分配,也确保了添加单个元素操的平均时间
是一个常数时间。


map

哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key
都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别
对应key和value。

map中所有的key都有相同的类型,所有的value也有着相同的类型,但是
key和value之间可以是不同的数据类型。

内置的make函数可以创建一个map:
ages := make(map[string]int) // mapping from strings to ints

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}

另一种创建空的map的表达式是 map[string]int{}

Map中的元素通过key对应的下标语法访问:
ages["alice"] = 32
fmt.Println(ages["alice"]) // "32"

使用内置的delete函数可以删除元素:
delete(ages, "alice") // remove element ages["alice"]

但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践
中,遍历的顺序是随机的,每一次遍历的顺序都不相同。

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践
中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍
历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我
们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。

map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它
们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常。

在向map存数据前必须先创建map。

通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到
与key对应的value;如果key不存在,那么将得到value对应类型的零值。


结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结
构体的成员。

type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee

dilbert结构体变量的成员可以通过点操作符访问。

或者是对成员取地址,然后通过指针访问:
position := &dilbert.Position
position = "Senior " + position // promoted, for outsourcing to Elbonia

点操作符也可以和指向结构体的指针一起工作:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
相当于下面语句
(*employeeOfTheMonth).Position += " (proactive team player)"

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决
定的。一个结构体可能同时包含导出和未导出的成员。

结构体类型往往是冗长的,因为它的每个成员可能都会占一行。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。
(该限制同样适应于数组。)但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我
们创建递归的数据结构,比如链表和树结构等。

结构体类型的零值是每个成员都对是零值如果考虑效率的话,
较大的结构体通常会用指针的方式传入和返回。

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函
数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回
结构体的地址:
pp := &Point{1, 2}

它是下面的语句是等价的
pp := new(Point)
*pp = Point{1, 2}

匿名成员:

Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就
叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构
体,同时Circle类型被嵌入到了Wheel结构体。
type Circle struct {
Point
Radius int
}

type Wheel struct {
Circle
Spokes int
}

时间: 2024-08-03 20:38:24

Go语言与数据库开发:01-04的相关文章

Go语言与数据库开发:01-01

一.前言 Google的三位大牛,为了解决在21世纪多核和网络化环境下越来越复杂的编程问题而发明了go语言, 从2007年9月开始设计和实现,于2009年的11月对外正式发布.从版本的发布历史来看,go语言是从 Ken Thompson发明的B语言.Dennis M. Ritchie发明的C语言逐步演化过来的,是C语言家族的成员, 因此很多人将Go语言称为21世纪的C语言. 纵观这几年来的发展趋势,Go语言已经成为云计算.云存储时代最重要的基础编程语言. Go语言有着和C语言类似的语法,但是它不

Go语言与数据库开发:01-09

包和工具 Go语言有超过100个的标准包(译注:可以用 go list std | wc -l 命令查看标准包的具体数 目),标准库为大多数的程序提供了必要的基础构件.在Go的社区,有很多成熟的包被设 计.共享.重用和改进,目前互联网上已经发布了非常多的Go语音开源包,它们可以通过http://godoc.org 检索. Go还自带了工具箱,里面有很多用来简化工作区和包管理的小工具. 包简介 任何包系统设计的目的都是为了简化大型程序的设计和维护工作,通过将一组相关的特性放 进一个独立的单元以便于

Go语言与数据库开发:01-02

接下来,开始了解go语言的程序结构,基础要打牢. Go语言和其他编程语言一样,一个大的程序是由很多小的基础构件组成的.变量保存值,简 单的加法和减法运算被组合成较复杂的表达式.基础类型被聚合为数组或结构体等更复杂的 数据结构.然后使用if和for之类的控制语句来组织和控制表达式的执行流程.然后多个语句被 组织到一个个函数中,以便代码的隔离和复用.函数以源文件和包的方式被组织. . 关于命名: 在Go中是区分大小写的:关键字不能用于自定义名字: Go语言的风格是尽量使用短小的名字,对于局部变量尤其

Go语言与数据库开发:01-08

基于共享变量的并发 .竞争条件 在一个线性(就是说只有一个goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定. 例如,我们有一段语句序列,第一个在第二个之前(废话),以此类推.在有两个或更多 goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但是一般情况 下我们没法去知道分别位于两个goroutine的事件x和y的执行顺序,x是在y之前还是之后还是 同时发生是没法判断的.当我们能够没有办法自信地确认一个事件是在另一个事件的前面或 者后面发生的

Go语言与数据库开发:01-07

在本节,我们来说一下并发. 并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要 Go语言中的并发程序可以用两种手段来实现.尽管Go对并发的支持是众多强力特性之一,但跟踪调试并发程序还是很困难,在线性程序中 形成的直觉往往还会使我们误入歧途. . Goroutines 在Go语言中,每一个并发的执行单元叫作一个goroutine.设想这里的一个程序有两个函数, 一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系.一个线性的程 序会先调用其中的一个函数,然后再调

Go语言与数据库开发:01-03

在本文中,我们将介绍go的基础数据类型. 虽然从底层而言,所有的数据都是由比特组成,但计算机一般操作的是固定大小的数,如整 数.浮点数.比特数组.内存地址等.进一步将这些数组织在一起,就可表达更多的对象, 例如数据包.像素点.诗歌,甚至其他任何对象.Go语言提供了丰富的数据组织形式,这依 赖于Go语言内置的数据类型.这些内置的数据类型,兼顾了硬件的特性和表达复杂数据结构 的便捷性. Go语言将数据类型分为四类:基础类型.复合类型.引用类型和接口类型. 本文将介绍基础类型,包括:数字.字符串和布尔

Go语言与数据库开发:01-06

Go语言包含了对OOP语言的支持,接下来我们来看看Go语言中的方法. 尽管没有被大众所接受的明确的OOP的定义,从我们的理解来讲,一个对象其实也就是一个 简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个一个和特殊类 型关联的函数.一个面向对象的程序会用方法来表达其属性和对应的操作,这样使用这个对 象的用户就不需要直接去操作对象,而是借助方法来做这些事情. 在函数声明时,在其名字之前放上一个变量,即是一个方法. 这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了

Go语言与数据库开发:01-10

测试 现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制.其中有两种技术在实践中证明是比较有效的.第一种是代码在被正式部署前需要进行代码评审.第二种则是测试 我们说测试的时候一般是指自动化测试,也就是写一些小的程序用来检测被测试代码(产品代码)的行为和预期的一样,这些通常都是精心设计的执行某些特定的功能或者是通过随机性的输入要验证边界的处理. 软件测试是一个巨大的领域.测试的任务可能已经占据了一些程序员的部分时间和另一些程序员的全部时间.和软件测试技术相关

Go语言与数据库开发:01-11

反射 Go语言提供了一种机制在运行时更新变量和检查它们的值.调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型.这种机制被称为反射.反射也可以让我们将类型本身作为第一类的值类型处理. Go语言的反射特性,看看它可以给语言增加哪些表达力,以及在两个至关重要的API是如何用反射机制的:一个是fmt包提供的字符串格式功能,另一个是类似encoding/json和encoding/xml提供的针对特定协议的编解码功能. 反射是一个复杂的内省技术,不应该随意使用,因此,尽管上面这些