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 模板

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年春秋杯网络安全联赛冬季赛 Writeup

文章目录 Webezezez_phppicup Misc谁偷吃了外卖modules明文混淆 Pwnnmanagerbook Reupx2023 CryptoCF is Crypto Faker 挑战题勒索流量Ezdede 可信计算 Web ezezez_php 反序列化打redis主从复制RCE&#xff1a;https://www.cnblogs.com/xiaozi/p/13089906.html <?php c…

教育大模型浪潮中,松鼠Ai的“智适应”故事好讲吗?

“计算机对于学校和教育产生的影响&#xff0c;远低于预期&#xff0c;要改变这一点&#xff0c;计算机和移动设备必须致力于提供更多个性化的课程&#xff0c;并提供有启发性的反馈。” 这是2011年5月份乔布斯与比尔盖茨最后一次会面时的记录&#xff0c;当时的电脑还十分落后…

大型语言模型 (LLM)全解读

一、大型语言模型&#xff08;Large Language Model&#xff09;定义 大型语言模型 是一种深度学习算法&#xff0c;可以执行各种自然语言处理 (NLP) 任务。 大型语言模型底层使用多个转换器模型&#xff0c; 底层转换器是一组神经网络。 大型语言模型是使用海量数据集进行训练…

Yuliverse:引领区块链游戏新篇章!

数据源&#xff1a;Yuliverse Dashboard 作者&#xff1a;lesleyfootprint.network 什么是 Yuliverse Yuliverse 是一款元宇宙游戏的先锋&#xff0c;是一款主打 Explore to earn 和 Social to earn 的链游。 这是一款能让你边玩边赚钱的免费区块链游戏&#xff0c;得到 LI…

如何在WordPress中使用 AI 进行 SEO(12 个工具)

您想在 WordPress 中使用 AI 进行 SEO 吗&#xff1f; 人工智能正在对 SEO 行业产生重大影响。已经有优秀的人工智能 SEO 工具&#xff0c;您可以使用它们来提高您的 SEO 排名&#xff0c;而无需付出太多努力。 在本文中&#xff0c;我们将向您展示如何通过我们精心挑选的工具…

代码随想录第十八天 513 找树左下角的值 112 路径之和 106 从中序与后序遍历序列构造二叉树

LeetCode 513 找树左下角的值 题目描述 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。 假设二叉树中至少有一个节点。 示例 1: 输入: root [2,1,3] 输出: 1示例 2: 输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 思路 1.确定递…

MySQL用户管理

1.用户 1.1 用户信息 mysql> use mysql; Database changed mysql> select host,user,authentication_string from user; --------------------------------------------------------------------- | host | user | authentication_string | --…

ubuntu 20.04 aarch64 平台交叉编译 libffi 库

前言 由于打算交叉编译 python&#xff0c;但是依赖 libffi 库&#xff0c;也就是 libffi 库也需要交叉编译 环境&#xff1a; ubuntu 20.04 交叉编译工具链&#xff1a;这里使用 musl libc 的 gcc 交叉编译工具链&#xff0c;aarch64-linux-musleabi-gcc&#xff0c;gcc 版本…

智谱AI官网再升级,GLM-4,智能体,AI作图长文档全部搞定

创建智能体 智能体体验中心 可以看到智谱AI也推出了自己的智能体&#xff0c;并且官方内置了丰富多样的智能体供大家免费体验。 GLM-4 原生支持自动联网、图片生成、数据分析等复杂任务&#xff0c;现开放体验中&#xff0c;快来开启更多精彩。写一篇《繁花》的影评&#xf…

[每日一题] 01.23 - 画矩形

画矩形 height,width,c,d input().split() height,width,d int(height),int(width),int(d) lis [c * width if d else c * (width - 2) c for i in range(height) ]lis: ##### # # # # ##### 或 # # # # # # # #if not d:print(c * width)for i in lis[1:-1…

1986-Minimum error thresholding

1 论文简介 《Minimum error thresholding》是由 Kittler 和 Illingworth 于 1986 年发布在 Pattern Recognition 上的一篇论文。该论文假设原始图像中待分割的目标和背景的分布服从高斯分布&#xff0c;然后根据最小误差思想构建最小误差目标函数&#xff0c;最后取目标函数最…

JAVAEE初阶 网络编程(三)

TCP回显服务器 一. TCP的API二. TCP回显服务器的代码分析三. TCP回显服务器代码中存在的问题四. TCP回显服务器代码五. TCP客户端的代码六.TCP为基准的回显服务器的执行流程 一. TCP的API 二. TCP回显服务器的代码分析 这的clientSocket并不是表示用户端的层面东西&#xff0c;…

kubernets集群搭建

集群搭建 1.准备工作(所有节点都执行)1.1配置/etc/hosts文件1.2关闭防火墙1.3关闭selinux1.4关闭交换分区&#xff0c;提升性能1.5修改机器内核参数1.6配置时间同步1.7配置阿里云镜像源 2.安装docker服务(所有节点都执行)2.1安装docker服务2.2配置docker镜像加速和驱动 3.安装配…

【分布式技术】消息队列Kafka

目录 一、Kafka概述 二、消息队列Kafka的好处 三、消息队列Kafka的两种模式 四、Kafka 1、Kafka 定义 2、Kafka 简介 3、Kafka 的特性 五、Kafka的系统架构 六、实操部署Kafka集群 步骤一&#xff1a;在每一个zookeeper节点上完成kafka部署 ​编辑 步骤二&#xff1a…

【GitHub项目推荐--微软开源的课程(Web开发课程/机器学习课程/物联网课程/数据科学课程)】【转载】

微软在 GitHub 开源了四大课程&#xff0c;面向计算机专业或者入门编程的同学。分别是 Web 开发课程、机器学习课程、物联网课程和数据分析课程。 四大课程在 GitHub 上共斩获 90K 的Star&#xff0c;每一课程包含 20 多小节&#xff0c;完成课程大约需要 12 周。每小节除了视…

如何解决Xshell 连接不上虚拟机Ubuntu?

一、 在终端输入 sudo apt-get install openssh-server 二、 执行如下命令 sudo apt-get install ssh 三、 开启 ssh-server&#xff0c;输入密码 service ssh start 四、 验证&#xff0c;输入 ps -e|grep ssh&#xff0c;看到sshd成功 ps -e|grep ssh五、 连接

【Linux编辑器-vim使用】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、vim的基本概念 二、vim的基本操作 分屏操作&#xff1a; 三、vim正常&#xff08;命令&#xff09;模式命令集 四、vim末行&#xff08;底行&#xff09;模…

【经验分享】MAC系统安装R和Rstudio(保姆级教程)安装下载只需5min

最近换了Macbook的Air电脑&#xff0c;自然要换很多新软件啦&#xff0c;首先需要安装的就是R和Rstudio啦&#xff0c;网上的教程很多很繁琐&#xff0c;为此我特意总结了最简单实用的安装方式: 一、先R后Rstudio 二、R下载 下载网址&#xff1a;https://cran.r-project.org …

shell脚本基础演练

简介 Shell脚本是一种用于自动化执行一系列命令的脚本语言。在Unix和类Unix系统中&#xff0c;常见的Shell包括Bash、Zsh、Sh等。下面我将简要讲解Shell脚本的基本结构和一些常用写法&#xff0c;并附上一些标准的例子。 基础示例 基本结构 #!/bin/bash # 注释: 这是一个简…