Golang 学习笔记二 字典 字符串 结构体

懒皮 · · 950 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、字典

《快学 Go 语言》第 6 课 —— 字典
1.make

func main() {
    var m map[int]string = make(map[int]string)
    fmt.Println(m, len(m))
}

----------
map[] 0

如果你可以预知字典内部键值对的数量,那么还可以给 make 函数传递一个整数值,通知运行时提前分配好相应的内存。这样可以避免字典在长大的过程中要经历的多次扩容操作。
var m = make(map[int]string, 16)

2.初始化

func main() {
    var m map[int]string = map[int]string{
        90: "优秀",
        80: "良好",
        60: "及格",  // 注意这里逗号不可缺少,否则会报语法错误
    }
    fmt.Println(m, len(m))
}

---------------
map[90:优秀 80:良好 60:及格] 3

3.读写

func main() {
    var fruits = map[string]int {
        "apple": 2,
        "banana": 5,
        "orange": 8,
    }
    // 读取元素
    var score = fruits["banana"]
    fmt.Println(score)

    // 增加或修改元素
    fruits["pear"] = 3
    fmt.Println(fruits)

    // 删除元素
    delete(fruits, "pear")
    fmt.Println(fruits)
}

-----------------------
5
map[apple:2 banana:5 orange:8 pear:3]
map[orange:8 apple:2 banana:5]

删除操作时,如果对应的 key 不存在,delete 函数会静默处理。遗憾的是 delete 函数没有返回值,你无法直接得到 delete 操作是否真的删除了某个元素。这时候必须使用字典的特殊语法,如下

func main() {
    var fruits = map[string]int {
        "apple": 2,
        "banana": 5,
        "orange": 8,
    }

    var score, ok = fruits["durin"]
    if ok {
        fmt.Println(score)
    } else {
        fmt.Println("durin not exists")
    }

    fruits["durin"] = 0
    score, ok = fruits["durin"]
    if ok {
        fmt.Println(score)
    } else {
        fmt.Println("durin still not exists")
    }
}

-------------
durin not exists
0

4.遍历
这个和数组一样的

func main() {
    var fruits = map[string]int {
        "apple": 2,
        "banana": 5,
        "orange": 8,
    }

    for name, score := range fruits {
        fmt.Println(name, score)
    }

    for name := range fruits {
        fmt.Println(name)
    }
}

------------
orange 8
apple 2
banana 5
apple
banana
orange

奇怪的是,Go 语言的字典没有提供诸于 keys() 和 values() 这样的方法,意味着如果你要获取 key 列表,就得自己循环一下,如下

func main() {
    var fruits = map[string]int {
        "apple": 2,
        "banana": 5,
        "orange": 8,
    }

    var names = make([]string, 0, len(fruits))
    var scores = make([]int, 0, len(fruits))

    for name, score := range fruits {
        names = append(names, name)
        scores = append(scores, score)
    }

    fmt.Println(names, scores)
}

----------
[apple banana orange] [2 5 8]
二、字符串

《快学 Go 语言》第 7 课 —— 字符串

image.png

1.按字节遍历

func main() {
    var s = "嘻哈china"
    for i:=0;i<len(s);i++ {
        fmt.Printf("%x ", s[i])
    }

}

-----------
e5 98 bb e5 93 88 63 68 69 6e 61

2.按字符 rune 遍历

package main

import "fmt"

func main() {
    var s = "嘻哈china"
    for codepoint, runeValue := range s {
        fmt.Printf("%d %d ", codepoint, int32(runeValue))
    }
}

-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97

对字符串进行 range 遍历,每次迭代出两个变量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示对应的 unicode 编码(类型是 rune)。

3.字符串是只读的
你可以使用下标来读取字符串指定位置的字节,但是你无法修改这个位置上的字节内容。如果你尝试使用下标赋值,编译器在语法上直接拒绝你。

package main

func main() {
    var s = "hello"
    s[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]

4.字节切片和字符串的相互转换
在使用 Go 语言进行网络编程时,经常需要将来自网络的字节流转换成内存字符串,同时也需要将内存字符串转换成网络字节流。Go 语言直接内置了字节切片和字符串的相互转换语法。

package main

import "fmt"

func main() {
    var s1 = "hello world"
    var b = []byte(s1)  // 字符串转字节切片
    var s2 = string(b)  // 字节切片转字符串
    fmt.Println(b)
    fmt.Println(s2)
}

--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world

从节省内存的角度出发,你可能会认为字节切片和字符串的底层字节数组是共享的。但是事实不是这样的,底层字节数组会被拷贝。如果内容很大,那么转换操作是需要一定成本的。

那为什么需要拷贝呢?因为字节切片的底层数组内容是可以修改的,而字符串的底层字节数组是只读的,如果共享了,就会导致字符串的只读属性不再成立。

5.修改字符串
无法直接修改每一个字符元素

angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
    angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))

字符串不可变有很多好处,如天生线程安全,大家使用的都是只读对象,无须加锁;再者,方便内存共享,而不必使用写时复制(Copy On Write)等技术;字符串 hash 值也只需要制作一份。所以说,代码中实际修改的是 []byte,[]byte 在 Go 语言中是可变的,本身就是一个切片。在完成了对 []byte 操作后,在第 9 行,使用 string() 将 []byte 转为字符串时,重新创造了一个新的字符串。

三、结构体

《快学 Go 语言》第 8 课 —— 结构体
1.结构体类型的定义
结构体和其它高级语言里的「类」比较类似。下面我们使用结构体语法来定义一个「圆」型

type Circle struct {
  x int
  y int
  Radius int
}

Circle 结构体内部有三个变量,分别是圆心的坐标以及半径。特别需要注意是结构体内部变量的大小写,首字母大写是公开变量,首字母小写是内部变量,分别相当于类成员变量的 Public 和 Private 类别。内部变量只有属于同一个 package(简单理解就是同一个目录)的代码才能直接访问。

2.创建

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

----------
{x:100 y:100 Radius:50}

可以只指定部分字段的初值,甚至可以一个字段都不指定,那些没有指定初值的字段会自动初始化为相应类型的「零值」。

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}

结构体的第二种创建形式是不指定字段名称来顺序字段初始化,需要显示提供所有字段的初值,一个都不能少。这种形式称之为「顺序形式」。var c Circle = Circle {100, 100, 50}

结构体变量创建的第三种形式,使用全局的 new() 函数来创建一个「零值」结构体,所有的字段都被初始化为相应类型的零值。var c *Circle = new(Circle)注意 new() 函数返回的是指针类型。

第四种创建形式,这种形式也是零值初始化,就数它看起来最不雅观。var c Circle

3.零值结构体和 nil 结构体
nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。

var c *Circle = nil

而零值结构体是会实实在在占用内存空间的,只不过每个字段都是零值。如果结构体里面字段非常多,那么这个内存空间占用肯定也会很大。

4.结构体的拷贝

func main() {
    var c1 Circle = Circle {Radius: 50}
    var c2 Circle = c1
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
    c1.Radius = 100
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)

    var c3 *Circle = &Circle {Radius: 50}
    var c4 *Circle = c3
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
    c3.Radius = 100
    fmt.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:100}
&{x:0 y:0 Radius:100}

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

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

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

字符串头的结构体形式,它在 64 位机器上将会占用 16 个字节

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

字典头的结构体形式

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

6.结构体的参数传递
函数调用时参数传递结构体变量,Go 语言支持值传递,也支持指针传递。值传递涉及到结构体字段的浅拷贝,指针传递会共享结构体内容,只会拷贝指针地址,规则上和赋值是等价的。下面我们使用两种传参方式来编写扩大圆半径的函数。

package main

import "fmt"

type Circle struct {
    x int
    y int
    Radius 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}

从上面的输出中可以看到通过值传递,在函数里面修改结构体的状态不会影响到原有结构体的状态,函数内部的逻辑并没有产生任何效果。通过指针传递就不一样。

7.结构体方法
Go 语言不是面向对象的语言,它里面不存在类的概念,结构体正是类的替代品。类可以附加很多成员方法,结构体也可以。

package main

import "fmt"
import "math"

type Circle struct {
 x int
 y int
 Radius 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 = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793

Go 语言不喜欢类型的隐式转换,所以需要将整形显示转换成浮点型,不是很好看,不过这就是 Go 语言的基本规则,显式的代码可能不够简洁,但是易于理解。
Go 语言的结构体方法里面没有 self 和 this 这样的关键字来指代当前的对象,它是用户自己定义的变量名称,通常我们都使用单个字母来表示。
Go 语言的方法名称也分首字母大小写,它的权限规则和字段一样,首字母大写就是公开方法,首字母小写就是内部方法,只能归属于同一个包的代码才可以访问内部方法。
结构体的值类型和指针类型访问内部字段和方法在形式上是一样的。这点不同于 C++ 语言,在 C++ 语言里,值访问使用句点 . 操作符,而指针访问需要使用箭头 -> 操作符。

8.关于GO如何实现面对对象的继承、多态,是个有趣的话题。参考go是面向对象语言吗?


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:懒皮

查看原文:Golang 学习笔记二 字典 字符串 结构体

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

950 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传