Go语言基础4 - 数据(基本数据结构)

概述

我们将用几节来学习Go语言基础,本文结构如下:

数据
    new 分配
    构造函数与复合字面
    make 分配
    数组
    切片
    二维切片
    映射
    打印
    追加
 初始化
    常量
    变量
    init 函数

数据

本节包含了 Go 为变量分配内存的方式,和常用的数组,map两种数据结构。

Go提供了两种分配方式,即内建函数 new 和 make。

关键点:

  • make 只适用于映射、切片和信道且不返回指针。
  • 若要获得明确的指针, 请使用 new 分配内存。

new 分配

new 函数格式为: new(T)
特点:它返回一个指针, 该指针指向新分配的,类型为 T 的零值

内建函数 new 是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。

Go 的 new比于java的情形是,java可以通过 new 执行构造来初始化一个对象,而Go不能初始化(赋初值),它只能置为”零值“

也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。用Go的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值

这样的设计,使得无需像Java那样面对不同对象的丰富多彩的构造函数和参数。

既然 new 返回的内存已置零,就不必进一步初始化了,使用者只需用 new 创建一个新的对象就能正常工作。

例如:

  • bytes.Buffer 的文档中提到“零值的 Buffer 就是已准备就绪的缓冲区。"
  • sync.Mutex 并没有显式的构造函数或 Init 方法, 而是零值的 sync.Mutex 就已经被定义为已解锁的互斥锁了。

p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

如上的两种方式,都会分配好内存空间,而类型是不同的。

构造函数与复合字面

有些场景下,仍然需要一个初始化构造函数,就像 os 包中的这段代码所示:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

上面的代码过于冗长。我们可通过复合字面来简化它:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

注意 File{fd, name, nil, 0} 这样的写法就是 复合字面的写法。该表达式在每次求值时都会创建新的实例。

复合字面的字段必须按顺序全部列出。但如果以 字段:值对的形式明确地标出元素,初始化字段时就可以按任何顺序出现,未给出的字段值将赋予零值。 因此,我们可以用如下形式:

return &File{fd: fd, name: name}

make 分配

内建函数 make 的格式为: make(T, args)
特点:它只用于创建切片、映射和信道,并返回类型为 T(而非 *T)的一个已初始化 (而非置零)的值。

切片、映射和信道 本质上为引用数据类型,在使用前必须初始化。 例如,切片是一个具有三项内容的描述符,包含一个指向(数组内部)数据的指针、长度以及容量, 在这三项被初始化之前,该切片为 nil。

对于切片、映射和信道,make 用于初始化其内部的数据结构并准备好将要使用的值。

例如:
make([]int, 10, 100) 分配一个具有100个 int 的数组空间,接着创建一个长度为10, 容量为100并指向该数组中前10个元素的切片结构
new([]int) 会返回一个指向新分配的,已置零的切片结构, 即一个指向 nil 切片值的指针。

下面的例子阐明了 new 和 make 之间的区别:

var p *[]int = new([]int)       // 分配切片结构;*p == nil;基本没用
var v  []int = make([]int, 100) // 切片 v 现在引用了一个具有 100 个 int 元素的新数组

// 没必要的复杂:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// 习惯用法:
v := make([]int, 100)

再次说明关键点:

  • make 只适用于映射、切片和信道且不返回指针。
  • 若要获得明确的指针, 请使用 new 分配内存。

数组

在规划内存布局时,数组是非常有用的,有时还能避免过多的内存分配, 在Go中,数组主要用作切片的构件,在构建切片时使用。

数组在Go和C中的主要区别。在Go中:

  • 数组是值。将一个数组赋予另一个数组会复制其所有元素。
  • 若将某个数组传入某个函数,它将接收到该数组的一份副本而非指针。
  • 数组的大小是其类型的一部分。类型 [10]int 和 [20]int 是不同的。

数组为值的属性很有用,但代价高昂;若你想要C那样的行为和效率,你可以传递一个指向该数组的指针。

在 Go 中,更习惯的的用法是使用 切片。

切片

切片通过对数组进行封装,为有序列的数据提供了更通用、强大而方便的方式。

除了矩阵变换这类需要明确维度的情况外,Go中的大部分数组编程都是通过切片来完成的。

切片保存了对底层数组的引用,若你将某个切片赋予另一个切片,它们会引用同一个数组。 若某个函数将一个切片作为参数传入,则它对该切片元素的修改对调用者而言同样可见, 这可以理解为传递了底层数组的指针。

修改长度:只要切片不超出底层数组的限制,它的长度就是可变的,只需产生新的切片再次指向自身变量即可。

切片的长度:

len(切片)

切片的容量可通过内建函数 cap 获得,它将给出该切片可取得的最大长度。函数为:

  cap(切片)

若数据超出其容量,则会重新分配该切片。返回值即为所得的切片。

向切片追加东西的很常用,因此有专门的内建函数 append。

一般情况下,如果我们要写一个 append 方法的话,最终返回值必须返回切片。示例:

      func Append(slice, data[]byte) []byte {
        l := len(slice)
        if l + len(data) > cap(slice) {  // 重新分配
            // 为了后面的增长,需分配两份。
            newSlice := make([]byte, (l+len(data))*2)
            // copy 函数是预声明的,且可用于任何切片类型。
            copy(newSlice, slice)
            slice = newSlice
        }
        slice = slice[0:l+len(data)]
        for i, c := range data {
            slice[l+i] = c
        }
        return slice
    }

如上,输入参数是切片和插入的元素值,返回值是切片,注意切片的长度会发生变化。
因为尽管 Append 可修改 切片 的元素,但切片自身(其运行时数据结构包含指针、长度和容量)是通过值传递的。

二维切片

要创建等价的二维数组或切片,就必须定义一个数组的数组, 或切片的切片,示例:

type Transform [3][3]float64  // 一个 3x3 的数组,其实是包含多个数组的一个数组。
type LinesOfText [][]byte     // 包含多个字节切片的一个切片。

每行都有其自己的长度:
由于切片长度是可变的,因此其内部可能拥有多个不同长度的切片。

映射 (map)

映射 是Go中 数据结构中的 map结构实现,即 key: value的形式存储。

映射的值可以是各种类型。
映射的键可以是整数、浮点数、复数、字符串、指针、接口等。

映射的键(或者叫索引)可以是任何相等性操作符支持的类型, 如整数、浮点数、复数、字符串、指针、接口(只要其动态类型支持相等性判断)、结构以及数组。 切片不能用作映射键,因为它们的相等性还未定义。与切片一样,映射也是引用类型。

如果将映射作为参数传入函数中,并更改了该映射的内容,则此修改对调用者同样可见。

映射可使用一般的复合字面语法进行构建,其键-值对使用逗号分隔,有点像JSON:

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

获取值:

offset := timeZone["EST"]

注意:若试图通过映射中不存在的键来取值,就会返回与该映射中项的类型对应的零值。例如,若某个映射包含整数,当查找一个不存在的键时会返回 0。

判断某个值是否存在:

seconds, ok = timeZone[tz]

上面是惯用的 "逗号 ok” 法:

  • 若 tz 存在, seconds 就会被赋予适当的值,且 ok 会被置为 true; - 若不存在,seconds 则会被置为零,而 ok 会被置为 false。

若仅需判断映射中是否存在某项而不关心实际的值,可使用空白标识符 _ 来代替该值的一般变量。

_, present := timeZone[tz]

要删除映射中的某项,可使用内建函数 delete。即便对应的键不在该映射中,此操作也是安全的。

delete(timeZone, "PDT")  

打印

Go的格式化打印风格和C的 printf 类似,但却更加丰富而通用。 这些函数位于 fmt 包中,且函数名首字母均为大写:如 fmt.Printf、fmt.Fprintf,fmt.Sprintf 等。

看例子:

  // 以f 结尾的这几个,传入格式化字符串作为参数, 不换行
fmt.Printf("hello, %v \n","zhang3")
fmt.Fprintf(os.Stdout,"hello, %v \n","zhang3")
str := fmt.Sprintf("hello, %v \n","zhang3")

//下面这几个,会换行
fmt.Println(str)
// 注意下面这个,会自动在元素间插入空格
fmt.Fprintln(os.Stdout,"f1","f2","f3")

Sprintf 用于构造字符串: 字符串函数(Sprintf 等)会返回一个字符串,而不是写入到数据流中。

Fprint 用于写入到各种流中:fmt.Fprint 一类的格式化打印函数可接受任何实现了 io.Writer 接口的对象作为第一个实参;比如 os.Stdout 与 os.Stderr 。

下面对 Printf 支持的格式化的字符做一些说明:
-- 格式: %d
像 %d 不接受表示符号或大小的标记, 会根据实际的类型来决定这些属性。

var x uint64 = 1<<64 - 1 // x 是无符号整数, 下面的 int64(x) 转换为有符合整数
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

将打印

18446744073709551615 ffffffffffffffff; -1 -1

-- 格式: %v
%v 可理解为 实际的 value。
它还能打印任意值,甚至包括数组、结构体和映射。

fmt.Printf("%v\n", timeZone)  // 或只用 fmt.Println(timeZone)

这会输出

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

%+v 和 %#v
当打印结构体时,格式 %+v 会带上每个字段的字段名,而格式 %#v 会带上类型。

  type T struct {
    a int
    b float64
    c string
  }
  t := &T{ 7, -2.35, "abc\tdef" }
  fmt.Printf("%v\n", t)
  fmt.Printf("%+v\n", t)
  fmt.Printf("%#v\n", t)

将打印

&{7 -2.35 abc   def} // 请注意其中的&符号
&{a:7 b:-2.35 c:abc     def} // 有了字段名
&main.T{a:7, b:-2.35, c:"abc\tdef"} //有了类型

-- 格式:%q
当遇到 string 或 []byte 值时, 可使用 %q 产生带引号的字符串;而格式 %#q 会尽可能使用反引号。

--格式:%x
%x 还可用于字符串、字节数组以及整数,并生成一个很长的十六进制字符串, 而带空格的格式(% x)还会在字节之间插入空格。

--格式: %T
它会打印某个值的类型.

fmt.Printf("%T\n", timeZone)

会打印

map[string] int

-- 为结构图自定义输出
类似 java 中的 toString(),对结构图自定义类型的默认格式,只需为该类型定义一个具有 String() string 签名的方法。对于我们简单的类型 T,可进行如下操作。

func (t *T) String() string {
        return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}

fmt.Printf("%v\n", t)

会打印出如下格式:

7/-2.35/"abc\tdef"

-- 任意数量的
Printf 的签名为其最后的实参使用了 ...interface{} 类型,这样格式的后面就能出现任意数量,任意类型的形参了。

func Printf(format string, v ...interface{}) (n int, err error) {

在 Printf 函数的实现中,v 看起来更像是 []interface{} 类型的变量,但如果将它传递到另一个变参函数中,它就像是常规实参列表了。实际上,它直接将其实参传递给 fmt.Sprintln 进行实际的格式化。

// Println 通过 fmt.Println 的方式将日志打印到标准记录器。
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output 接受形参 (int, string)
}

注意上面的 ...interface{} 和 v... 的写法。

追加 ( append 函数 说明 )

append 函数的签名就像这样:

func append(slice []T, 元素 ...T) []T

其中的 T 为任意给定类型的占位符。实际上,你无法编写一个类型 T 由调用者决定的函数。这也就是为何 append 为内建函数的原因:它需要编译器的支持。
append 会在切片末尾追加元素并返回结果。我们必须返回结果, 原因是,底层数组可能会被改变(注意数组的长度是类型的一部分)。

以下简单的例子

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

将打印

[1 2 3 4 5 6]

将一个切片追加到另一个切片很简单:在调用的地方使用 ...

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

如果没有 ...,它就会由于类型错误而无法编译,因为 y 不是 int 类型的。三个点符号 “ ... ” 的作用有点像“ 展开 ” 的作用,即将 y这个切片的元素放到了这里。

初始化

GO 的huaGo的初始化很强大,在初始化过程中,不仅可以构建复杂的结构,还能正确处理不同包对象间的初始化顺序。

常量

常量在编译时被创建,即便函数中定义的局部变量也一样。
常量只能是数字、字符(符文)、字符串或布尔值。

由于编译时的限制, 定义它们的表达式必须是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式。

枚举常量
枚举常量使用枚举器 iota 创建。由于 iota 可为表达式的一部分,而表达式可以被隐式地重复,这样也就更容易构建复杂的值的集合了。

  type ByteSize float64

  const (
      // 通过赋予空白标识符来忽略第一个值
      _           = iota // ignore first value by assigning to blank identifier
      KB ByteSize = 1 << (10 * iota)
      MB
      GB
      TB
      PB
      EB
      ZB
      YB
  )

变量

变量的初始化与常量类似,但其初始值也可以是在运行时才被计算的一般表达式。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

init 函数

每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。格式为:

func init() { 
    ...
 }

而 init 方法执行结束,就意味着初始化结束了:只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而那些 init 只有在所有已导入的包都被初始化后才会被求值。

init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。示例:

  func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath 可通过命令行中的 --gopath 标记覆盖掉。
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
  }

END

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容