Go 复合数据类型

1. 数组(array)(OK)

数组
数组的概念数组是具有固定长度且拥有零个或多个相同数据类型元素的序列

i.  元素的数据类型相同

ii. 长度固定的序列 

iii. 零个或多个元素的序列

与 slice 对比

由于数组的长度固定,所以在 Go 里面很少直接使用;

slice 的长度可以增长和缩短,故 slice 使用得更多

定义数组语法

var 数组名 [数组长度]元素类型

var arrName [arrLen]eType

访问单个元素数组中的每个元素是通过索引(下标)来访问的,索引从 0 到数组长度减 1
获取数组长度Go 内置的函数 len 可以返回数组中的元素个数
代码示例

var  a  [3]int                         // 定义长度为 3 的 int 型数组

fmt.Println(a[0])                    // 输出第一个元素

fmt.Println(a[len(a) - 1])        // 输出最后一个元素

// 输出索引和元素

for i, v := range a {

    fmt.Print("%d %d\n", i, v)

}

// 仅输出元素,丢弃索引

for _, v := range a {

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

}

数组的零值

默认情况下,一个新数组中的元素初始值为元素类型的零值;

数字的零值为 0;字符串的零值为 "" ;布尔的零值为 false

声明并初始化

可以使用数组字面量,即根据一组值来初始化一个数组;

若元素个数与数组长度不一致,缺省的元素为元素类型的零值

var  q [3]int = [3]int{1,2,3}

var r [3]int = [3]int{1,2}

fmt.Println(r[2])    // "0"

确定数组长度

在声明数组的同时给出数组长度;

var  a  [3]int

用数组字面量初始化,如果省略号 "..." 出现在数组长度的位置,

那么数组的长度由初始化数组的字面量中的元素个数决定;

注意:省略号 "..." 只能出现在数组字面量的数组长度的位置

q := [...]int{1,2,3}

var p [...]int = [3]int{1,2,3}  // 编译错误

数组长度示例

q := [...]int{1,2,3}

fmt.Printf("%T\n", q)    // "[3]int"        使用 %T 来打印对象的类型

数组长度特别说明

i. 数组的长度是数组类型的一部分,即长度是数组的固有属性

[3]int 和 [4]int 是两种不同的数组类型

ii. 数组的长度必须是常量表达式

这个常量表达式的值在程序编译时就必须确定

数组类型示例

q  :=  [3]int{1,2,3}

q = [4]int{1,2,3,4}    // 编译错误:不可以将 [4]int 赋值给 [3]int

数组字面量的

默认值

(索引-值 初始化)

数组 、slice 、map 、结构体 的字面语法都是相似的;

上面的数组例子,是按顺序给出一组值;

也可以向下面这样给出一组元素,元素同时具有索引和值

// 声明类型别名

type  Currency  int

// 定义一组常量

const (

    USD  Currency  = iota

    EUR

    GBP

    RMB

)

// 声明数组,用数组字面量初始化

symbol  :=  [...]string{USD :"$",EUR :"€",GBP :"£",RMB :"¥"}

fmt.Println(RMB,symbol[RMB])    //  "3  ¥"

在这种情况下,元素(索引-值)可以按照任意顺序出现,索引有时候还能省略;

没有指定值的索引位置的元素,其值为数组元素类型的零值

// 下标为 99 的元素值为 -1 ,则前 99 个元素均为 0

r  :=  [...]int{99:-1}   

数组的比较

如果一个数组的元素类型是可比较的,那么这个数组也是可比较的

可以直接使用 " == " 操作符来比较两个(同类型的)数组,比较的结果是两边元素的值是否完全相同

使用 " != " 来比较两个数组是否不一样

不同类型(长度不同 或 元素类型不同)的数组不能比较,否则会编译报错

代码示例

a  :=  [2]int{1,2}

b  := [...]int{1,2}

c  := [2]int{1,3}

fmt.Println(a == b ,a == c ,b == c)    //    "true  false  false"

d  :=  [3]int{1,2}     // d[2] == 0

fmt.Println(a  == d)    // 编译错误 :无法比较 [2]int == [3]int

数组比较

真实示例

举一个更有意义的例子,crypto/sha256 包里面的函数 Sum256 用来为存储在任意字节 slice 中的消息使用 SHA256 加密散列算法生成一个摘要。摘要信息为 256 位,即 [32]byte 。如果两个摘要信息相同,那么很有可能这两条原始消息就是相同的;如果这两个摘要信息不同,那么这两条原始消息就是不同的。

下面的程序输出并比较了 "x" 和 "X" 的 SHA256 散列值:

import "crypto/sha256"

func main() {

    c1  :=  sha256.Sum256([]byte("x"))

    c2  :=  sha256.Sum256([]byte("X"))

    fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)

}

这两个原始消息仅有一位(bit)之差,但是它们生成的摘要消息有将近一半的位不同

注意,上面的格式化字符串 %x 表示将一个数组或者 slice 里面的字节按照十六进制的方式输出,%t  表示输出一个布尔值,%T  表示输出一个值的类型

Go 函数默认

传值调用

当调用一个函数的时候,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量(形参),所以函数接受的是一个副本,而不是原始的参数;

函数使用传值调用的方式接收大的数组会变得很低效,

并且在函数内部对数组的任何修改都仅仅影响副本,而不是原始数组

这种情况下,Go 函数把数组和其他的类型都看成 "值传递"
在其他语言中,数组都是隐式第使用引用传递

传递指针给函数

直接修改原始数据

当然,也可以显式地传递一个 "数组的指针" 给函数,这样在函数内部,对数组的任何修改都会反映到原始数组上
数组清零程序

下面的程序演示如何将一个数组 [32]byte 的元素清零

func zero(ptr *[32]byte) {

    for i := range ptr {

        ptr[i] = 0

    }

}

数组字面量 [32]byte{} 可以生成一个拥有 32 个字节元素的数组;

数组中每个元素的值都是字节类型的零值,即 0

另一个版本的数组清零程序:

func zero(ptr *[32]byte) {

    *ptr = [32]byte{}

}

使用数组指针

i.  使用数组指针是高效的;

ii. 允许被调函数修改调用方数组中的元素;

数组很少被使用

因为数组长度是固定的,所以数组本身是不可变的;

例如,上面的 zero 函数不能接收一个 [16]byte 这样的数组指针,

也无法为数组添加或删除元素;

由于数组的长度不可改变,除了在特殊的情况下,很少使用数组

2. 切片(slice)

slice
slice 基本概念slice 表示一个拥有相同类型元素的 " 可变长度 " 的序列

slice 通常写成  [ ]T ,其中元素的类型是 T ;

看上去像没有长度的数组类型

底层数组数组和 slice 是紧密关联的

slice 是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素;

而这个数组称为 slice 的 " 底层数组 "

slice 的属性slice 有 3 个属性 :指针 、长度 、容量

指针 :指向数组的第一个可以从 slice 中访问的元素

注意 :这个元素不一定是数组的第一个元素

长度 :slice 中的元素个数,长度不能超过 slice 的容量
容量 :通常是从 slice 的起始元素到底层数组的最后一个元素之间的元素个数
lenGo 的内置函数 len 用来返回 slice 的长度
capGo 的内置函数 cap 用来返回 slice 的容量

一个底层数组可以对应多个 slice ,这些 slice 可以引用数组的任何位置;

这些 slice ,彼此之间的元素还可以重叠

下图展示了月份名称的字符串数组和两个元素存在重叠的 slice ;

数组声明如下 :

// 索引式的数组字面量

months := [...]string{1:"January" ,/* ... */ ,12:"December"}

所以 January 就是 months[1] ,December 是 months[12] ;

一般来讲,数组中索引 0 的位置存放数组的第一个元素,但由于月份是从 1 开始的,

因此我们可以不设置索引为 0 的元素,这样 months[0] 的值就为 " "

创建 sliceslice 操作符 s[ i : j ]( 其中,0\leq i\leq j\leq cap(s) )创建了一个新的 slice
这个新的 slice 引用了序列 s 中,从 ij - 1 索引位置的所有元素
这里的 s 既可以是数组,或者指向数组的指针,也可以是其他的 slice

起始/结束

索引位置缺省

新 slice 的元素个数是 j - i 个

如果表达式 s[ i : j ] 中省略了 i ,则新的 slice 的起始索引位置为 0 ,即 i = 0 ;

s[ : j ] 相当于 s[ 0 : j ] 

如果表达式 s[ i : j ] 中省略了 j ,则新的 slice 的结束索引位置为 len(s) - 1 ,

即 j = len(s) ;

s[ i : ] 相当于 s[ i : len(s)  ]

说明

因此,slice months[1:13] 引用了所有的有效月份;

同样的写法可以是 months[1:]

slice months[ : ] 引用了整个数组
接下来,我们定义元素重叠的 slice ,分别用来表示第二季度的月份,北半球的夏季月份:

Q2  :=  months[ 4 : 7 ]                // 一年中的第二季度是,4 、5 、6 这三个月

summer  :=  months[ 6 : 9 ]        // 北半球的夏季是 6 、7 、8 这三个月

fmt.Println(Q2)            // [  "April"   "May"   "June"  ]

fmt.Println(summer)    // [   "June"   "July"   "August"   ]

元素 "June" 同时被包含在两个 slice 中;

用下面的代码来输出两个 slice 中的共同元素(虽然效率不高)

for _,s := range summer {

    for _,q := range Q2 {

        if s == q {

            fmt.Printf("%s appears in both\n",s)

        }

    }

}

越界分类如果 slice 的引用超过了被引用对象的容量,即 cap(s) ,那么会导致程序宕机;
如果 slice 的引用超过了被引用对象的长度,即 len(s) ,那么最终 slice 会比原先的 slice 长
代码示例

fmt.Println(summer[ : 20 ])               // 宕机 :超过了被引用对象的边界

endlessSummer  :=  summer[ : 5]    // 在 slice 容量范围内扩展了 slice

fmt.Println(endlessSummer)             // "[June July August Septmber October]"

子串与切片

另外,注意

i. 求字符串(string)子串操作

ii. 对字节 slice( [ ]byte )做 slice 操作

这两者的相似性

相同点

它们都写作 x[m:n] ;

都返回原始字节的一个子序列 ;

同时两者的底层引用方式也是相同的,所以两个操作都消耗常量时间

不同点

如果 x 是字符串,那么 x[m:n] 返回的是一个字符串;

如果 x 是字节 slice ,那么 x[m:n] 返回的是一个字节 slice ;

因为 slice 包含了指向数组元素的指针,所以将一个 slice 传递给函数的时候,

可以在函数内部修改底层数组的元素;

也就是说,创建一个数组的 slice ,等于为数组创建了一个别名

下面的函数 reverse 就地反转了整型 slice 中的元素,它适用于任意长度的整型 slice

// 就地反转一个整型 slice 中的元素

func reverse( s [ ]int ) {

    for i ,j  :=  0 ,len(s) - 1 ;i < j ;i ,j = i + 1 ,j - 1 {  // i++ ,j--

        s[ i ] ,s[ j ] = s[ j ] ,s[ i ]

    }

}

// 这里反转整个数组 a :

a  :=  [...]int{0,1,2,3,4,5}

reverse( a[ : ] )

fmt.Println( a )  //  "[ 5 4 3 2 1 0 ]"

将一个 slice 左移 n 个元素的简单方法 :连续调用 reverse 函数三次

第一次反转前 n 个元素,第二次反转剩下的元素,最后对整个 slice 再做一次反转

如果将 slice 右移 n 个元素,那么先做上面的第三次调用

s  :=  [ ]int{0,1,2,3,4,5}

// 左移两个元素

reverse( s[ : 2 ] )

reverse( s[ 2 : ] )

reverse( s )

fmt.Println( s )    //  "[  2 3 4 5 0 1 ]"

注意,初始化 slice  s 的表达式和初始化数组 a 的表达式,两者的区别

slice 字面量 、数组字面量 ,两者很相似;

都是用逗号分隔,并用花括号 "{ }" 括起来的一个元素序列;

但是 slice 没有指定长度

// 声明并初始化数组

a  :=  [...]int{0,1,2,3,4,5}

// 声明并初始化 slice

s  :=  [ ]int{0,1,2,3,4,5}

这种隐式区别的结果分别是创建具有固定长度的数组,创建指向数组的 slice
和数组一样,slice 可以按照顺序指定元素 ,也可以通过索引指定元素,或两者结合

和数组不同的是,slice 无法做比较;

因此不能用 " == " 来测试两个 slice 是否拥有相同的元素(个数以及对应值都相等)

自行比较 slice

标准库里面提供了高度优化的函数 bytes.Equal 来比较两个字节 slice( [ ]byte );

但是,对于其他类型的 slice ,我们必须自己写函数来比较两个 slice

func equal( x,y [ ]string ) bool {

    if len(x) != len(y) {  // 长度不同则不同

        return false

    }

    for i := range x {

        if x[ i ] != y[ i ] {

            return false

        }

    }

    return true

}

这种深度比较看上去很简单,并且运行的时候并不比字符串数组使用 " == " 做比较多耗费时间,那为什么 slice 的比较不可以直接使用 " == " 操作符做比较呢?

这里有两个原因:

原因一 :slice 是对底层数组的引用

和数组元素不同,slice 的元素是 "非直接的" ,有可能 slice 可以包含它自身;

虽然有办法处理这种特殊的情况,但是没有一种方法是简单 、高效 、直观的

原因二 :

由于 slice 的元素不是直接的,如果底层数组元素改变,则同一个 slice 在不同的时间会拥有不同的元素

由于散列表(例如 Go 中的 map 类型) 仅对元素的键做浅拷贝,这就要求,散列表里面的键,在散列表的整个生命周期内必须保持不变

因为 slice 需要深度比较,所以就不能用 slice 作为 map 的键

对于引用类型,例如指针和通道,操作符 " == " 检查的是 "引用相等性" ,即它们是否指向同一个对象;

如果有一个相似的 slice 相等性比较功能,它或许会比较有用,也能解决 slice 作为 map 键的问题,但是如果操作符 " == " 对 slice 和数组的行为不一致,会带来困扰

所以,最安全的方法就是,不允许直接比较 slice

比较操作slice 唯一允许的比较操作,是和 nil 作比较
if summer == nil  { /* ... */ }
slice 零值slice 类型的零值是 nil 
值为 nil 的 sice 没有对应的底层数组
值为 nil 的 slice ,其长度为 0 ,其容量为 0

也有值不是 nil 的 slice ,其长度和容量都是零;

例如 [ ]int{ } 或 make( [ ]int ,3 )[ 3 : ]  

对于任何类型,如果它们的值可以是 nil ,那么这个类型的 nil 值可以使用一种转换表达式,例如 [ ]int(nil)

var  s  [ ]int         // len(s) == 0 ,s == nil

s  =  nil               // len(s) == 0 ,s == nil

s  =  [ ]int( nil )    // len(s) == 0 ,s == nil

s  =  [ ]int{ }         // len(s) == 0 ,s != nil

空 slice 

所以,如果想检查一个 slice 是否为空,那么使用 len(s) == 0 ,而不是 s == nil ;

因为 s != nil 的情况下,slice 也有可能为空

除了可以和 nil 作比较之外,值为 nil 的 slice ,其行为,与其他长度为 0 的 slice 一样
例如,reverse 函数调用 reverse( nil ) 也是安全的
除非文档上面说明了与此相反,否则无论值是否为 nil ,Go 的函数都应该以相同的方式对待所有长度为 0 的 slice 

内置make函数创建slice

内置函数 make 可以创建一个具有指定 元素类型长度容量 的 slice

其中,容量参数可以省略,则 slice 的长度和容量相等

make( [ ]T,len )

make( [ ]T,len,cap )  //  和 make( [ ]T,cap )[ : len ] 功能相同

深入研究下,其实 make 创建了一个无名数组,并返回了它的一个 slice ;

这个数组只能通过这个 slice 进行访问

// 返回的 slice 引用了整个数组

make( [ ]T,len )

// 只引用了数组的前 len 个元素,但是 slice 的容量是数组的长度,预留了空间

make( [ ]T,len,cap ) 

2.1 append 函数
append 函数

2.2 slice 就地修改

3. 字典(map)

字典(map)
创建 map
方式一

内置函数 make 可以用来创建一个(空)map

students := make(map[int]string)    // 学号到名字的映射

students["Jake"] = 31  // 添加元素

students["Mike"] = 54  // 添加元素

方式二

使用 map 的字面量来新建一个带初始化键值对的字典

students := map[int]string {

        1 : "Alice",

        2 : "Bob",

        3 : "Charlie",

        4 : "David",

        5 : "Eva",

        6 : "Frank",

}

方式三

直接创建一个新的空 map

students := map[string]int{}

students["Jake"] = 31  // 添加元素

students["Mike"] = 54  // 添加元素

添加元素

students["Jake"] = 31  // 添加元素

students["Mike"] = 54  // 添加元素

如上面的操作所示:

如果键不存在,则为新增元素

如果键存在,则为修改元素

访问 map 元素
访问元素

使用 key(类似数组下标)来访问 map 中对应的 value

students["Mike"] = 55

fmt.Println(students["Mike"])

map 使用给定的键来查找(访问)元素,如果对应的元素(键)不存在,则使用该元素(键值对),值为零值;即 value 初始化为默认值

话句话说:使用不存在的元素(通过 key 访问)相当于新增元素,value 为默认值

示例:下面的代码可以正常工作,尽管 "Bob" 还不是 map 的键,此时使用

teachers["Bob"] 的值为 0

teachers["Bob"] = teachers["Bob"] + 1

fmt.Println(teachers["Bob"])  //   " 1 "

复合赋值运算(如 x += y 和 x++)对 map 中的元素同样适用

teachers["Bob"] += 1  或  teachers["Bob"]++

移除元素(键值对)

delete

移除元素

可以使用内置函数 delete ,根据键,从 map 中移除一个元素(键值对)

即使键不在 map 中,delete 操作也是安全的

语法: delete(map_name,key)

delete(students,"Mike")  // 移除元素 students["Mike"]

delete(students,"Carl")  // 键 "Carl" 不存在,但这么操作是允许的

map 中的元素不是变量
value 不是变量

但是 map 元素不是一个变量,不可以获取 value 的地址

错误操作如下:

_ = &teachers["Bob"]    // 编译错误,无法获取 map 元素的地址

无法获取 map 元素地址的第一个原因:

map 的增长可能会导致已有元素被重新散列到新的存储位置,这样的话,之前获取的地址(可能已经存在某个变量中)与当前地址不一致(前面的地址无效)

遍历 map

使用迭代 for

循环遍历 map

可以使用 for 循环(结合 range 关键字,迭代 for 循环)来遍历 map 中所有的键和对应的值

(就像遍历 slice 一样,range 遍历 slice ,返回下标和值;range 遍历 map ,返回键和值)

示例 :循环语句的每一次迭代,会将键赋予 name ,将值赋予 age

for name,age := range teachers {

    fmt.Printf("%s\'s age is %d\n", name,age)

}

注意

map 中元素的迭代顺序是不固定的,不同的实现方法会使用不同的散列算法,得到不同的元素顺序

实践中,我们认为这种顺序是随机的,从一个元素开始到后一个元素,依次执行;

这种设计是有意为之,这样可以使得程序在不同的散列算法实现下更健壮

按照键的顺序

有序遍历 map

如果需要按照某种顺序来遍历 map 中的元素,必须显式地来给键排序

例如,如果键是字符串类型,可以使用 sort 包中的 Strings 函数来进行键的排序

import "sort"

var names [ ]string

for name := range teachers {

      names = append(names,name)  // 先把 map 中的 Key 都保存在一个 slice 中

}

sort.Strings(names)  // 使保存 key 的 slice 变得有序

for _,name := range names {

    // 遍历有序的 slice ,依次获得 name ,再通过 name 访问 map 中的 age 

    fmt.Printf("%s\'s age is %d\n",name,teachers[name])

}

优化:

因为一开始就知道 slice names 的长度,所以可以直接指定一个 slice 的长度,这样更高效

语法: make(容器类型,初始长度,容器容量)   

            make(type,len,cap)

下面的语句,创建了一个初始元素为空,但容量足以容纳 map 中所有键的 slice

names := make([ ]string ,0 ,len(teachers))

第一个循环中,我们只需要 map teachers 的所有键,所以忽略了循环中的第二个变量;

第二个循环中,我们需要使用 slice names 中的元素值,所以使用空白符 _ 来忽略第一个变量,即元素索引

range 可以返回一个值,也可以返回两个值;

返回一个值:遍历序列则只返回下标,遍历字典则只返回键

返回两个值:遍历序列同时返回下标和元素,遍历字典同时返回键和值

只需要第一个值,则只返回一个值;只需要第二个值,则用 _ 来接收第一个值

    m := map[int]int {

        1 : 100,

        2 : 200,

        3 : 300,

    }

    s := make([]int, 0, len(m))

    for v := range m {

        s = append(s, v)

        fmt.Printf("v is %d\n", v)

    }

    for k := range s {

        fmt.Printf("vkis %d\n", k)

    }

    for _, v := range s {

        fmt.Printf("%d : %d\n", v, m[v])

    }

map 是引用类型

map 类型的零值是 nil ,也就是说,没有引用任何散列表

// 只是定义了 map 变量,未分配内存(map 是散列表的引用)

// 未绑定具体的散列表,所以结果是:编译通过,运行失败

var name = "carol"

var m map[string]int

m[name] = 21    //  宕机:为零值 map 中的项赋值,编译通过,运行失败

fmt.Println(m == nil)    //   "true"

// {} 即为一个空的散列表,绑定到变量 m 上

// 结果是:编译通过,运行成功

var name = "carol"

m := map[string]int{}    // 初始化

m[name] = 21

fmt.Println(m == nil)    //   "false"

// make 函数根据类型 map[string]int 分配了一个空的散列表,绑定到变量 m 上

// 结果是:编译通过,运行成功

var name = "carol"

m := make(map[string]int)    //  初始化

m[name] = 21

fmt.Println(m == nil)    //   "false"

fmt.Printf("%s' age is %d\n", name, m[name])

carol's age is 21

大多数的 map 操作都可以安全地在 map 的零值 nil 上执行,包括查找元素,删除元素,获取 map 元素个数( len ),执行 range 循环等等,因为这和在空 map 上的行为一致

但是,向零值 map 中设置元素会导致错误

var name = "carol"

var m map[string]int    // 零值 map ,未初始化

m[name] = 21    //  宕机:为零值 map 中的项赋值,编译通过,运行失败

fmt.Println(m == nil)    //   "true"

设置元素之前,必须初始化 map

访问 map

注意点

通过下标(键)的方式访问 map 中的元素总是会有值;

如果键在 map 中,则获得键对应的值;

如果键不在 map 中,则获得 map 值类型的零值

判断 map 中是否存在某个元素
判断元素存在

有时候需要知道一个元素是否在 map 中

例如,如果元素类型是数值类型,需要辨别一个不存在的元素,或者恰好这个元素的值是 0

age,ok := teachers["Bob"]

if !ok {

    /* "Bob" 不是字典中的键,age == 0 */

}

合并成一条语句:

if age,ok := teachers["Bob"];!ok {

    /*  ...  */

}

通过这种下标方式访问 map 中的元素输出两个值,第二个值是一个布尔值,用来报告该元素是否存在;这个布尔值一般叫作 ok ,尤其是它(ok)立即用在 if 条件判断中的时候

比较 map

map 比较操作

和 slice 一样,(两个)map 不可比较
唯一合法的比较就是,map 变量与 nil 做比较

为了判断两个 map 是否拥有相同的键和值,必须写一个循环:

func equal(x,y map[string]bool)  bool  {

        if len(x) != len(y) {

                return false    // 两个 map 长度不等,则这两个 map 不相等

        }

        for k,xv := range x {    // 某一个键 k 和键对应的值 xv

                // 键 k 对应的值 yv 是否存在;yv 存在的情况下,yv 与 xv 是否相等

                // yv 不存在,或 yv 存在但与 xv 不相等

                if yv,ok := y[k];!ok || yv != xv {  // 错误写法 :xv != y[k]

                        return false

                }

        }

        return true

}

注意,如何使用 !ok 来区分 "元素不存在" 和 "元素存在但值为零" 的情况;

如果简单写成了 xv != y[k] ,那么下面的调用将错误地报告两个 map 相等

// 如果 equal 函数写法错误,结果为 True

equal(map[string]int{"A" : 0},map[string]int{"B" : 42})

使用 map 构造集合类型(Set)
Go 没有提供集合类型
既然 map 的键都是唯一的,就可以用 map 来实现这个功能

示例:

为了模拟这个功能,程序 dedup 读取一系列的行,并且只输出每个不同行一次;

程序 dedup 使用 map 的键来存储这些已经出现过的行,来确保接下来出现的相同行不会输出

func main() {

        seen := make(map[string]bool)    // 字符串集合

        input := bufio.NewScanner(os.Stdin)

        for input.Scan() {

                line := input.Text()

                if !seen[line] {

                        seen[line] = true

                        fmt.Println(line)

                }

        }

        if err := input.Err();err != nil {

                fmt.Fprintf(os.Stderr,"dedup: %v\n",err)

                os.Exit(1)

        }

}

Go 程序员通常把这种使用 map 的方式描述成字符串集合;

但是请注意,并不是所有的 map[string]bool 都是简单的集合,有一些 map 的值会同时包含 true 和 false 的情况

4. 结构体(struct)

结构体基础
结构体概念

结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型

每个变量都叫做结构体的成员
现实例子在数据处理领域,结构体使用的经典实例是员工信息记录,记录中有唯一 ID 、姓名 、地址 、出生日期 、职位 、薪水 、直属领导等信息;所有的这些员工信息成员都作为一个整体组合在一个结构体中

结构体

整体操作

(1). 可以复制一个结构体(变量)

(2). 将结构体变量传递给函数 

(3). 结构体变量作为函数的返回值 

(4). 将结构体变量存储到数组中,等等

结构体声明

示例

下面的语句定义了一个叫 Employee 的结构体和一个结构体变量 dilbert :

type Employee struct {

    ID                 int

    Name           string

    Address       string

    DoB             time.Time

    Position       string

    Salary          int

    ManagerID  int

}

var dilbert Employee

访问成员

结构体对象的每一个成员都通过句点( . )方式进行访问

fmt.Println(dilbert.Name)

结构体对象是一个变量,其所有成员也都是变量,因此可以给结构体的成员赋值

dilbert.Salary -= 5000    // 代码量减少,降薪

获取成员变量的地址,然后通过指针来访问

position := &dilbert.Position

*position = "Senior " + *position    // 工作外包给 Elbonia ,所以升职

句号( . )同样可以用在结构体指针上

var employeeOfTheMonth *Employee = &dilbert

employeeOfTheMonth.Position += "  (proactive team player)"

后面一条语句等价于:

(*employeeOfTheMonth).Position += "  (proactive team player)"

函数 EmployeeID 通过给定的参数 ID 返回一个指向 Employee 结构体的指针;

可以用句号( . )来访问其(结构体指针)成员变量

func EmployeeByID(id int) *Employee { /* ... */ }

fmt.Println(EmployeeByID(dilbert.ManagerID).Position)

id := dilbert.ID

EmployeeByID(id).Salary = 0

最后一条语句更新了函数 EmplyeeByID() 返回的指针指向的结构体 Employee;

如果函数 EmployeeByID() 的返回值类型变成了 Employee 而不是 *Employee ,那么代码将无法通过编译,因为赋值表达式的左侧无法识别出一个变量

结构体的成员变量,通常一行写一个,变量名称在类型的前面;

相同类型的连续成员变量可以写在一行上

type Employee struct {

    ID                            int

    Name,Address     string

    DoB                         time.Time

    Position                   string

    Salary                      int

    ManagerID               int

}

成员变量(声明)的顺序对于结构体同一性(是否为同一个类型)很重要

如果将同为字符串类型的 Position 和 Name、Address 组合在一起或者互换了 Name 和 Address 的顺序,那么就是定义了一个不同的结构体类型

一般来说,我们只会组合相关的成员变量

如果一个结构体的成员变量名称首字母大写,那么这个变量是可导出的(public);

这个是 Go 最主要的访问控制机制;

一个结构体可以同时包含可导出(首字母大写,public)和不可导出(首字母小写,private)的成员变量

因为在结构体类型中,通常一个成员变量占据一行,所以结构体的定义比较长;

虽然可以在每次需要它(结构体完整定义)的时候写出整个结构体类型定义,即 "匿名结构体类型" ,但是重复完全没必要;所以通常我们会定义命名结构体类型,比如 Employee

命名结构体类型 S 不可以定义一个拥有相同结构体类型 S 的成员变量,也就是一个聚合类型不可以包含它自己(同样的限制对数组也适用)

但是 S 中可以定义一个 S 的指针类型,即 *S 

这样就可以创建一些递归数据结构,比如链表和树

下面的代码给出了一个利用二叉树来实现插入排序的例子

type tree struct {

    value         int

    left,right  *tree

}

// 就地排序

func Sort(values [ ]int) {

    var root  *tree

    for _,v := range values {

        root = add(root,v)

    }

    appendValues(values[:0],root)

}

// appendValues 将元素按照顺序追加到 values 里面,然后返回结果 slice

func appendValues(values [ ]int,t  *tree) [ ]int {

    if t != nil {

        values = appendValues(values,t.left)

        values = append(values,t.value)

        values = appendValues(values,t.right)

    }

    return values

}

func add(t  *tree,value int) *tree {

    if t == nil {

        // 等价于返回 &tree{value : value}

        t = new(tree)

        t.value = value

        return t

    }

    if value < t.value {

        t.left = add(t.left,value)

    } else {

        t.right = add(t.right,value)

    }

}

结构体的零值,由结构体成员的零值组成;

通常情况下,我们希望零值是一个默认的、自然的、合理的值;

例如,在 bytes.Buffer 中,结构体的初始值就是一个可以直接使用的空缓存;

有时候,这种合理的初始值实现简单,但是有时候也需要类型的设计者花费时间来进行设计

没有任何成员变量的结构体,称为 "空结构体" ,写做 struct{ }
空结构体没有长度,也不携带任何信息,但有时候会很有用

有一些 Go 程序员用空结构体来替代被当作集合使用的 map 中的布尔值,来强调只有 "Key" 是有用的(不需要 value)

但由于这种方式节约内存很少且语法复杂,所以一般尽量避免这么用

seen := make(map[string]struct{ })  // 字符串集合

// ...

if _,ok := seen[s];!ok {

    seen[s] = struct{ }

    // ...首次出现 s...

}

4.1 结构体字面量
结构体类型的值,可以通过 "结构体字面量" 来设置,即通过设置结构体的成员变量来设置

type Point struct{ X ,Y  int }

p  :=  Point{ 1 ,2 }

结构体字面量格式
格式一

按照正确的顺序,为每个成员变量指定一个值

type Point struct{ X ,Y  int }

p  :=  Point{ 1 ,2 }    //  1 是 X ,2 是 Y

缺点:

这会给开发和阅读代码的人增加负担,因为他们必须记住每个成员变量的顺序

这也使得未来结构体成员变量扩充或者重新排列的时候,代码维护性变差

应用:

所以,这种结构体字面量格式一般用在定义结构体类型的包中,或者一些有明显的成员变量顺序约定的小型结构体中

比如 image.Point{x,y} 或 color.RGBA{red,green,blue,alpha}

格式二

用得更多的是第二种格式,通过指定部分或者全部成员变量的名称和值,来初始化结构体变量

anim  :=  gif.GIF{LoopCount : nframes}

在这种初始化方式中,如果某个成员没有指定初始值,那么该成员的值就是该成员类型的零值;

因为指定了成员变量的名字,所以它们的顺序是无所谓的

注意

两种初始化方式不可以混合使用;

不可以使用第一种初始化方式来绕过规则:不可导出变量无法在其他包中使用

package p

type T struct{ a ,b  int }    // a 和 b 都是不可导出的

package q

import "p"

var _ = p.T{ a : 1 ,b : 2 }    // 编译错误,无法引用 a 、b,小写已提示,不可用

var _ = p.T{ 1 ,2 }              // 编译错误,无法引用 a 、b,小写未提示,也不可用

虽然上面的最后一行代码没有显式地提到不可导出变量,但是它们被隐式地引用了,所以这也是不允许的

结构体类型的值,可以作为参数传递给函数或者作为函数的返回值

下面的函数将 Point 缩放了一个比率

func Scale(p Point ,factor int) Point {

    return Point{p.X * factor ,p.Y * factor}

}

fmt.Println(Scale(Point{1,2},5))    //  "{5,10}"

处于效率的考虑,大型的结构体,通常都使用结构体指针的方式,直接传递给函数,或者从函数中返回

(传递结构体指针给函数 、函数返回结构体指针)

func Bonus(e *Employee ,percent int) int {

    return e.Salary * percent / 100

}

使用结构体指针的方式,在函数需要修改结构体内容的时候是必须的

在 Go 这种按值调用的语言中,调用的函数接收到的是实参的一个副本,并不是实参的引用(在函数内修改形参,不会影响函数外实参的状态)

func AwardAnnualRaise(e *Employee) {

    e.Salary = e.Salary * 105 / 100

}

由于通常结构体都通过指针的方式使用,因此可以使用一种简单的方式来创建 、初始化一个 struct 类型的变量,并获取它的地址

pp := &Point{ 1,2 }

等价于:

pp := new(Point)

*pp = Point{ 1,2 }

但是 &Point{ 1,2 } 这种方式可以直接在一个表达式中使用,例如函数调用

4.2 结构体比较
如果结构体的所有成员变量都是可以比较的,那么这个结构体就是可比较的

两个结构体可以使用 == 或者 != 进行比较

其中 == 操作符按照顺序比较两个结构体变量的成员变量

所以,下面的两个输出语句是等价的:

type Point struct{ X ,Y  int }

p := Point{ 1,2 }

q := Point{ 2,1 }

fmt.Println(p.X == q.X && p.Y == q.Y)    // "false"

fmt.Println(p == q)                                   // "false"

和其他可比较的类型一样,可比较的结构体类型都可以作为 map 的键类型

type address struct {

    hostname  string

    port            int

}

hits := make(map[address]int)

hits[address{"golang.org" ,443}]++

4.3 结构体嵌套和匿名成员

5. JSON

6. 文本和 HTML 模板

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/342230.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c# ADODB.Recordset实例调用Fields报错

代码&#xff1a; using System; using System.CodeDom; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ADODB;namespace ConsoleApp1 {internal class Programre{static ADODB.Recordset recordsetInstance…

el-table分组合并行

接到一个需求&#xff0c;把数据按照天 分组显示 时间单独一行&#xff0c;数据在一块 这里新知识点&#xff1a;span-method const plist ref([{ dateHeader:2024-01-23, list:[{dateHeader:2024-01-23},{name:数据1,id:1},{name:数据2,id:2}] }, { dateHeader:2024-01-24,…

国产芯片电子秤方案CSU8RP1185

在生活中&#xff0c;买菜时常常出现缺斤少两的情况&#xff0c;这种情况多是商家秤有很大问题&#xff0c;往往消费者是最吃亏的&#xff0c;这种情况下&#xff0c;我们最好是带个吊钩电子秤&#xff0c;测量菜的重量&#xff0c;有问题直接拨打举报电话举报商家&#xff0c;…

muduo网络库剖析——线程Thread类

muduo网络库剖析——线程Thread类 前情从muduo到my_muduo 概要框架与细节成员函数使用方法 源码结尾 前情 从muduo到my_muduo 作为一个宏大的、功能健全的muduo库&#xff0c;考虑的肯定是众多情况是否可以高效满足&#xff1b;而作为学习者&#xff0c;我们需要抽取其中的精…

【ElasticSearch】 ElasticSearch serverless架构介绍(查询写入分离,计算存储分离)

ElasticSearch 推出了全新的serverless架构&#xff0c;将查询(search)和写入(indexing)分离&#xff0c;将计算(computing)和存储(storage)分离&#xff0c;极大提高了 ES 的可运维性&#xff0c;降低了学习成本。本文将先介绍下serverless含义&#xff0c;再介绍ElasticSearc…

ESP8266模块双模式(AP+STA)共存同时与电脑及手机进行UDP通信

1.准备工作: 硬件: ESP8266模块 USB连接线: 连接ESP8266模块到电脑 如果电脑没有USB接口,准备一个USB HUB: USB HUB 连接电脑Type-C接口,ESP8266模块连接USB HUB 软件: 安装Arduino IDE 2.2.1 在Arduino IDE中安装esp8266开发板(USB没识别芯片,要安装对应操作系统CH2340或…

LiveVideoStack人物专访:深耕多媒体二十载,他怎么看未来的视频云?

抓住已知的&#xff0c;迎面未知的。 编者按&#xff1a; 大模型、降本、出海&#xff0c;是多媒体从业者交流的高频词&#xff0c;内容与交互的需求层出不穷&#xff0c;大模型与AI的演进目不暇接&#xff0c;让增速低走的视频云迎面新的机遇和挑战。作为一个跨越中美多媒体行…

【面试深度解析】字节后端日常实习一面这么问吗?

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复…

上门回收小程序,打造回收新模式

近年来&#xff0c;我国一直秉持着环保绿色的发展理念&#xff0c;为了减少资源浪费&#xff0c;旧物回收成为了人们处理废弃物品的方式。目前&#xff0c;我国回收市场规模大约能达到3.58亿元&#xff0c;在我国经济的稳定增长和环保意识的提高下&#xff0c;回收市场规模还将…

力扣每日一题 --- 972. 相等的有理数

本题中的一个难点是怎么判断是否相等&#xff0c;如果自己写判断的话是不是很麻烦&#xff0c;判断整数之后再去判断小数部分&#xff0c;那么我们这题的另一个难点就要登场了&#xff0c;第一个难点让本题的情况变得复杂&#xff0c;第二个难点让本题变得很难想到怎么判断&…

网络安全B模块(笔记详解)- 内存取证

1.从内存文件中获取用户admin的密码并破解,将该密码作为flag值提交(密码长度为6个字符); 2.获取内存文件中系统的IP地址,将IP地址作为flag值提交; 3.获取内存文件中系统的主机名,将主机名作为flag值提交; 4.内存文件的系统中存在挖矿进程,将矿池的IP地址作为flag值提…

qml:FocusInput、TextInput 键盘输入

有2个输入框&#xff0c;默认焦点在第一个输入框&#xff0c;按Tab键可以在两个输入框之间来回切换。 FocusInput.qml import QtQuickFocusScope { //显式创建焦点范围width: 200height: 40x: 20y: 20property alias text: input.textproperty alias input: inputRectangle…

DNA序列修正*

题目 import java.util.HashMap; import java.util.Map; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();sc.nextLine();char[] sq1 sc.next().toCharArray();sc.nextLine(…

低代码(Low-Code)技术简化开发难度,快速搭建应用

目录 一、低代码技术定义 二、低代码技术优势 1.提高企业的工作效率 2.降低企业的开发成本 3.提高应用程序和业务流程的质量 三、稳定性和生产率的最佳实践 三、最后 随着数字化时代的到来&#xff0c;低代码&#xff08;Low-Code&#xff09;技术已经成为了企业数字化转…

科技、文化与旅游的融合创新:智慧文旅的未来之路

在当今社会&#xff0c;科技、文化与旅游的融合已经成为文旅产业转型升级的重要趋势。这种融合不仅有助于提升文旅产业的核心竞争力&#xff0c;更有助于推动产业的数字化转型和可持续发展。 本文将深入探讨科技、文化与旅游的融合创新&#xff0c;以及智慧文旅场景的解决方案…

使用 Swift 代码优化项目编译速度

引言 软件的性能是评价一个软件质量的重要指标&#xff0c;尤其在今天这个时代&#xff0c;性能已成为大型项目不可或缺的考虑因素之一。对于用户量极大的软件&#xff0c;如网银系统、在线购物商城等&#xff0c;更是必须保证其高效稳定的性能。在这种背景下&#xff0c;优化…

【MySQL·8.0·源码】subquery 子查询处理分析(一)

引言 在 SQL 中&#xff0c;子查询属于 Nested Query 的一种形式&#xff0c;根据 Kim 的分类[1]&#xff0c;Nested Query 即嵌套查询是一种 SQL-like 形式的查询语句嵌套在另一 SQL 中&#xff0c;SQL-like 的嵌套子句可以出现在 SELECT、FROM 和 WHERE 子句的任意位置。 在…

C++: vector

目录 1.vector的介绍 2.vector常用的接口 1.vector构造 2.迭代器iterator的使用 3.vector空间增长 4.vector的增删改查 3.vector模拟实现 如果在reverse时使用memcpy会怎么样&#xff1f; 1.vector的介绍 C中的vector是一个动态数组容器&#xff0c;可以存储任意类型的…

【算法分析与设计】二叉树的层序遍历

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;算法分析与设计 ⛺️稳中求进&#xff0c;晒太阳 题目 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xf…

Python 自动化办公:一键批量生成 PPT

Stata and Python 数据分析 一、导读 在实际工作中&#xff0c;经常需要批量处理Office文件&#xff0c;比如需要制作一个几十页的PPT进行产品介绍时&#xff0c;一页一页地制作不仅麻烦而且格式可能不统一。那么有什么办法可以一键生成PPT呢&#xff1f;Python提供的pptx 包…