快速入门Go(3)

Go·语法 2019-01-06 11367 字 2518 浏览 点赞

前言

这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。

此篇包括了Go相关的:指针,数组,切片,字典,结构体等。涉及关键字struct

指针

Go语言中的指针与C稍异。如C中,声明一个int类型的指针语句:int* p ,所以p的类型就为int*。而Go语言中,*在前面:

func main() {
    num := 512
    var p *int  // Go语言中‘*’在前面
    p = &num
    fmt.Printf("address of num is %p", p)
}

由于Go语言在声明变量的时候会自动初始化值,对于指针,会指向nil,而不是NULL(Go中没有NULL):

func main() {
    var p *int
    fmt.Printf("address of pointer is %p, and its content is %v", p, p)
}
// 输出:
address of num is 0x0, and its content is <nil>

Go语言不允许操作没有合法指向的内存:

func main() {
    var p *int
    *p = 512  // 对p指针指向的内存赋值
   fmt.Printf("address of pointer is %p, and its content is %v", p, p)
}
// 抛异常:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x490056]

new

new()函数是用来分配内存的内建函数,用法为new(T)(T是类型),会将分配的内存置为0值,同时返回该内存的地址

func main() {
    p := new(int)  // 返回*int指针
    fmt.Printf("type of pointer is %T\n", p)
    fmt.Printf("content of memory is %v\n", *p)
}
// 输出:
type of pointer is *int
content of memory is 0

此时你可以正确操作指针p指向的内存,比如正确的赋值:

*p = 512  // 不可以赋值字符串

从C语言角度理解这个过程,可以近似为:

int main() {
    int *p = (int*)malloc(sizeof(int));  // 申请内存
    memset(p, 0, sizeof(int));  // 内存置0操作
    printf("address of memory is %p, content of memory is %d", *p, *p);
    free(p);  // 释放操作
    return 0;
}

重点在于,Go语言有完善的GC机制,不需要手动释放内存;而C中,每一个malloc后,都应该有对应的free操作。

数组

Go中的数组声明与C也不同:

// Go
var array [3]int  // 声明一维数组
var array [2][3]int  // 声明二维数组

var array = [3]int{1, 2, 3}  // 对一维数组赋值
array := [3]int{1, 2, 3}  // 简短声明一维数组

var array = [2][3]  // 对二维数组赋值
array := [2][3]int{{1, 2, 3}, {3, 2, 1}}  // 简短声明一维数组

var arr = [...]int{1, 2, 3, 4}  // 允许使用`...`自动计算长度
arr := [...]int{1, 2, 3, 4}

// C
int array[3];  // 声明一维数组
int array[2][3];  // 声明二维数组
int array[3] = {1, 2, 3};
int array[2][3] = {{1, 2, 3}, {3, 2, 1}};

对Go中的数组,有以下几点需要注意:

  • 数组声明时不可以用变量表示元素个数,如:
// 错误操作
n := 10
array := [n]int
  • 数组可以指定元素初始化(未指定元素默认0值),如:
func main() {
    array := [5]int{2: 512, 4: 1024}  // 冒号前边的数表示索引值
    for i := range array {
        fmt.Printf("%d ", array[i])
    }
}
// 输出:
0 0 512 0 1024
  • 数组支持==!=运算,前提是数组类型要一样,即[x][y]type(eg:2int) 一致:
func main() {
    arr1 := [2][3] int {{1, 2, 3}, {3, 2, 1}}
    arr2 := [2][3] int {{1, 2, 3}, {3, 2, 1}}
    fmt.Println("arr 1 == arr2 ?", arr1 == arr2)

    arr3 := [2][3] int {{2, 1, 3}, {3, 2, 1}}
    arr4 := [2][3] int {{1, 2, 3}, {3, 2, 1}}
    fmt.Println("arr 3 == arr4 ?", arr3 == arr4)
}
// 输出
arr 1 == arr2 ? true
arr 3 == arr4 ? false
  • 同类型的数组允许相互赋值:
var arr1 = [2]int {1, 2}
var arr2 [2]int
arr2 = arr1

Go语言中,传递数组是值传递

func change(arr [2]int) {
    arr[0] = 512
}

func main() {
    arr := [2]int{}
    fmt.Println("before call change(): ", arr[0])

    change(arr)
    fmt.Println("after call change(): ", arr[0])
}
// 输出:
before call change():  0
after call change():  0  // 值未被改变

然而C语言中,传递数组是指针传递

void change(int arr[])
{
    arr[0] = 512;
}

int main() {
    int arr[2] = {0};
    printf("before call change(): %d\n", arr[0]);
    change(arr);
    printf("after call change(): %d\n", arr[0]);
}
// 输出:
before call change(): 0
after call change(): 512  // 值被改变

切片

切片与数组的区别

区别1:声明数组时,中括号写明了数组的长度或使用...自动计算长度,声明slice时,中括号内没有任何字符,如:var s = []int或者s := []int{}

区别2:array静态数组拥有固定的长度,达到容量限制之后就无法再添加元素;Go语言中的切片与Python的list概念很相似,可以通过扩容的方式突破容量限制(也可以认为因为扩容而使得容量值一直在改变)。

区别3:作为函数的参数传递时,数组是值传递,切片为引用传递。

创建切片的方式

var方式

var s = []int

简短声明

s := []int{}

make方式

s := make([]int, 3, 5)
fmt.Printf("切片的长度:%d,切片的容量:%d\n", len(s), cap(s))

// 输出:
切片的长度:3,切片的容量:5

make的第一个参数表示切片类型,第二个参数表示切片的长度,第三个参数表示切片的容量。

第四种:对一个现有数组切片

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

切片的分析

切片由三部分组成:指针,长度,容量。指针指向slice对应的底层数组(中的某个元素),长度表示slice中一共存放了多少个元素,容量表示slice最多可以存放多少个元素。当slice中的元素数量超过容量时,会重新开辟一个大于当前元素数量的空间来存放现有元素。

不妨先从元素个数大于容量开始说起:

func main() {
    var s []int  // 声明一个切片
    fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))

    s = append(s, 1, 2, 3)  // 向切片里追加1,2,3
    fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))

    s = append(s, 4)
    fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))

    s = append(s, 5)
    fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
}
// 输出
切片的首地址:0x0,切片的长度:0,切片的容量:0
切片的首地址:0xc0000580c0,切片的长度:3,切片的容量:4
切片的首地址:0xc0000580c0,切片的长度:4,切片的容量:4
切片的首地址:0xc000088100,切片的长度:5,切片的容量:8

可以看到,当往切片里存放三个元素后(1,2,3),切片s的长度为3,容量为4;然后添加一个元素(4),s长度为4,容量为4,此时前后两次,s的地址是相同的。再往切片追加一个元素(5),超过了切片原有容量,于是s重新找一块地“占山为王”,新的“山头”可以容纳更多人,因此你看到了最后一次输出结果:s有了新的地址,也有了新的容量值。

以上方式创建的切片会让我们察觉不到底层数组的存在,因此我们可以换一种方式创建切片来感受它与数组间的关联:

 arr := [5]int{1, 2, 3, 4, 5}
 s := arr[1:3:4]  // 重点看这一句

如果你会Python,你可能感到熟悉,但这并非我们熟悉的Python,这是Go语言。其中1表示从数组arr的下标为1的元素起(也就是说,切片的指针指向该数组的第二个元素);3表示对数组arr的坐标取值到3-1的位置(这一点倒与Python的切片一样);最后一个4表示设置切片的容量,容量为:4-1

因此这里有一个格式:s := arr[low:high:max]。切片s长度为:high - low,容量为:max - low。而max需要小于等于数组arr的长度。

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s := arr[1: 3: 4]
    fmt.Printf("数组第2个元素的地址:%p\n", &arr[1])
    fmt.Printf("切片的首地址:%p,切片的长度:%d,切片的容量:%d\n", s, len(s), cap(s))
    fmt.Printf("切片的内容:%v", s)
}
// 输出:
数组第2个元素的地址:0xc00006e068
切片的首地址:0xc00006e068,切片的长度:2,切片的容量:3
切片的内容:[2 3]

切片作为底层数组引用的存在,能够影响数组的值。现在我们的切片s就引用了数组arr,改动s就是在改动arr:

fmt.Println("数组:", arr)
fmt.Println("切片:", s)

s[0] = 512  // 改变切片
fmt.Println("数组:", arr)
fmt.Println("切片:", s)
// 输出:
数组: [1 2 3 4 5]
切片: [2 3]
数组: [1 512 3 4 5]
切片: [512 3]

需要记住的是,只有在s还是arr的引用时才有这样的作用。比方如果往s添加元素以致超过了切片原有容量,切片会重新划分空间,s就不再是arr的引用,不会再影响arr

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s := arr[1:3:4]

    fmt.Printf("数组的内容:%v\n", arr)
    fmt.Printf("切片的内容:%v\n", s)
    fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))

    s = append(s, 10,)  // 切片追加1个元素,此时还未超过切片的容量
    fmt.Println()
    fmt.Printf("数组的内容:%v\n", arr)
    fmt.Printf("切片的内容:%v\n", s)
    fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))

    s = append(s, 11,)  // 再追加1个元素,超过切片原有容量
    fmt.Println()
    fmt.Printf("数组的内容:%v\n", arr)
    fmt.Printf("切片的内容:%v\n", s)
    fmt.Printf("切片的首地址:%p,切片的容量:%d\n", s, cap(s))
}
// 输出:
数组的内容:[1 2 3 4 5]
切片的内容:[2 3]
切片的首地址:0xc00000c368,切片的容量:3

数组的内容:[1 2 3 10 5]  // 数组值被影响
切片的内容:[2 3 10]
切片的首地址:0xc00000c368,切片的容量:3

数组的内容:[1 2 3 10 5]  // 数组值未被影响
切片的内容:[2 3 10 11]
切片的首地址:0xc00000c3f0,切片的容量:6

切片的用法

操作含义
s[n]切片 s 中索引位置为 n 的元素
s[:]从切片 s 的索引位置 0 到 len(s)-1 处获得的切片
s[low:]从切片 s 的索引位置 low 到 len(s)-1 处获得的切片
s[:high]从切片 s 的索引位置 0 到 high 处获得的切片, len=high
s[low:high]从切片 s 的索引位置 low 到 high 处获得的切片,len=high-low
s[low:high:max]从切片 s 的索引位置 low 到 high 处获得的切片,len = high-low, cap=max-low
len(s)切片 s 的长度,总是<=cap(s)
cap(s)]切片 s 的长度,总是>=len(s)

切片的两个方法

append:追加效果

s = append(s, x, y, z, ...)  // 切片 s 中追加x, y, z 等元素

s = append(s, n...)  // 切片 s 中追加切片 n 中的元素

copy:实现覆盖效果

x_slice := []int{1, 2, 3}
y_slice := []int{10, 20, 30, 40}
copy(y_slice, x_slice)
fmt.Println(y_slice)

// 输出:
[1 2 3 40]

x_slice中三个元素覆盖掉了y_slice中的前三个元素。

切片的其他说明

  • 切片仅允许与nil使用==!=,即便是类型相同的两个切片之间也不能使用比较运算符;
  • 同类型的切片之间允许相互赋值(s1)。

map

键值对数据类型,与Python中的字典类型(dict_ = {})相似。

创建map的方式

var方式

var m map[int]string  // 无自定义初始化值
var m = map[int]string{1:"bobby"}  // 有自定义初始化值

简短声明

m := map[int]string{}
m := map[int]string{1: "boby"}

make方式

m := make(map[int]string)
m := make(map[int]string, 2)  // 指定容量大小

中括号([])内为键类型,中括号的右边为值类型。


需要注意以下几点:

  • 不允许向一个nil值的map存入元素,会引发panic
var m map[int]string
fmt.Println(m == nil)
m[1] = "bobby"
fmt.Println(m)
// 输出:
true
panic: assignment to entry in nil map

解决方法,用以下方式创建map:

var m  = map[int]string{}
m := map[int]string{}
m := make(map[int]string)
  • 当有m[1] = "boby"语句时,如果m中没有关键字1,就会动态创建一个关键字为1值为"boby"的键值。如果没有指定map容量,map可能会频繁扩容来满足添加元素的需求,效率会降低,所以建议用m := make(map[int]string, capValue)方式创建字典,指明对map的预期容量(尽管如此,却不能对map类型使用cap()方法);
  • map类型的键值应该是确定并且唯一。确定:不应该用切片、函数等作为键值;唯一:同一个map中,不会有相同的键值;
  • map类型的变量作为参数实参传递时,用引用传递方式;
  • map类型的变量内部是无序的(与Python字典无序一样)。

判断键值是否存在

func main() {
    m := map[int]string{
        0: "bobby",
        1: "nike",
    }

    value, ok := m[1]  // 重点
    if ok {
        fmt.Printf("m[1]的值为:%v\n", value)
        fmt.Printf("ok 的值为:%v\n", ok)
    } else {
        fmt.Printf("不存在m[1]")
    }
}

如果键值1存在,则ok为true,否则false

删除键值对

delete(m, 1)

第一个参数为map类型的变量,第二个参数为键值。无论删除的键值是否存在m中,都不会引发panic

map的遍历

for key, value := range m{ 
    // ... 
}

结构体

定义一个结构体

结构体的声明方式:

type StructName struct {
    // ...
}

此时StructName是一种数据类型。放一个完整示例:

func main() {
    type student struct {
        name string
        age int
        id uint64
    }  // 定义一个名为 student 的结构体

    var s student  // 声明一个 student 类型的变量 s
    s.name = "zty"
    s.age = 22
    s.id = 201431060011

    fmt.Println(s)
}
// 输出:
{zty 22 201431060010}

通过.操作访问成员变量。这与C语言中的结构体仅仅存在细微差异:

int main()
{
    typedef struct {
        char* name;
        int age;
        unsigned long long id;
    } student;

    student s;
    s.name = "zty";
    s.age = 22;
    s.id = 201431060010;

    printf("%s %d %lld", s.name, s.age, s.id);
}
// 输出:
zty 22 201431060010

结构体变量的声明

var s  = student{"zty", 22, 201431060010}

s := student{"zty", 22, 201431060010}

s := new(student)  // 注意,此时s是指针类型

与C中结构体存在最大的差别是:Go语言中,指向结构体的指针访问成员时使用.而不是->

C语言如下:

student stu;  // stu 是一个结构体
student* s = &stu;  // s 是指向 stu 结构体的指针
s->name = "zty";  // `->` 的访问方式
s->age = 22;
s->id = 201531060010;

而Go语言:

var stu student
s := &stu
s.name = "zty"
s.age = 22
s.id = 201431060010
fmt.Println(s)
// 输出:
&{zty 22 201431060010}  // 注意这个取地址符

关于结构体变量初始化值也是有讲究的。要求顺序初始化必须对每个成员初始化:

s := student{"zty", 22, 20140010}  // 正确
s := student{"zty"}  // 错误

指定成员初始化中,允许不对每个成员初始化(未初始化的成员默认0值):

s := student{age:22}
fmt.Println(s)
// 输出:
{ 22 0}  // 这里面有三个值,第一个空字符串(name),第二个22(age),第三个0(id)

结构体的其他说明

  • 允许结构体参与==!=运算,但不能><
  • 同类型的结构体变量可以指相互赋值(s1 = s2);
  • 结构体作为函数实参传递时,使用值传递方式;
  • 结构体需要对外部可见时,无论是类型名还是成员名都必须首字母大写(type Student struct { Name string})。

随机数

Go中获取随机数分两步骤:

  • 设置随机种子;
  • 获取随机数。
import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())  // 设置随机种子
    // time.Now().UnixNano() 表示系统时间,因为时间总是在变化,借此得到不同的随机种子
    for i:=0; i < 3; i++ {
        fmt.Println(rand.Intn(100))
    }
}

rand.Intn(n)表示在0~n范围内获取随机数。



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论