Go 语言入门指南:基础语法和常用特性解析-五大复合类型

111 阅读9分钟

参考链接:【GO语言入门到精通】-CSDN博客

Go语言的五大复合类型:

image.png

现在从array,slice,pointer,map,struct的顺序依次介绍这几种复合类型的一些相关知识。本篇文章适合有一点编程基础的同学,并不适合出学编程语言的小白。

一、array

数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数组的长度。数组⻓度必须是常量,且是类型的组成部分。 [2]int 和 [3]int 是不同类型。

数组的定义和初始化

    //定义
    var n int = 10  //方式一
    var b [10]int  //方式二
    
    //初始化
    a := [3]int{1, 2}           // 未初始化元素值为 0
    b := [...]int{1, 2, 3}      // 通过初始化值确定数组长度
    c := [5]int{2: 100, 4: 200} // 通过索引号初始化元素,未初始化元素值为 0
    fmt.Println(a, b, c)        //[1 2 0] [1 2 3] [0 0 100 0 200]
 
    //支持多维数组
    d := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
    e := [...][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}} //第二维不能写"..."
    f := [4][2]int{1: {20, 21}, 3: {40, 41}}
    g := [4][2]int{1: {0: 20}, 3: {1: 41}}
    fmt.Println(d, e, f, g)

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。这里和java一样。

但是有一点需要值得注意:根据内存和性能来看,在函数间传递数组是一个开销很大的操作。数组在函数之间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。这里与java不一样。

在java语言中,数组代表的是一个地址值,通过对地址值的映射找到对应的数组里面的值。

换一个角度说明,当数组作为函数参数传递时,Go语言中仅仅是值传递,不能修改数组内的值,但是在java中却可以。下面分别从java和go语言的代码运行示例来说明在这一点:

//Go语言
func modify(array [5]int) {
    array[0] = 10 // 试图修改数组的第一个元素
    //In modify(), array values: [10 2 3 4 5]
    fmt.Println("In modify(), array values:", array)
}
 
func main() {
    array := [5]int{1, 2, 3, 4, 5} // 定义并初始化一个数组
    modify(array)                  // 传递给一个函数,并试图在函数体内修改这个数组内容
    //In main(), array values: [1 2 3 4 5]
    fmt.Println("In main(), array values:", array)
}

//java语言
public static void main(String[] args) {
    int[] a={1,2,3,4,5};
    System.out.println("a = " + a[0]);//输出为1

    int[] change = change(a);
    System.out.println("change = " + change[0]);//输出为2
}

public static int[] change(int[] a){
    a[0]=2;
    return a;
}

在go语言中想要实现对数组值修改,可以传递数组的地址值,示例代码如下:

func modify(array *[5]int) {
    (*array)[0] = 10
    //In modify(), array values: [10 2 3 4 5]
    fmt.Println("In modify(), array values:", *array)
}
 
func main() {
    array := [5]int{1, 2, 3, 4, 5} // 定义并初始化一个数组
    modify(&array)                 // 数组指针
    //In main(), array values: [10 2 3 4 5]
    fmt.Println("In main(), array values:", array)
}

二、slice

数组的长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。Go语言提供了数组切片(slice)来弥补数组的不足。

切片并不是数组或数组指针,它通过内部指针和相关属性引⽤数组片段段,以实现变长方案。

slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array,slice的声明也可以像array一样,只是不需要长度。

slice和数组的区别:声明数组时,方括号内写明了数组的长度或使用…自动计算长度,而声明slice时,方括号内没有任何字符。

    var s1 []int //声明切片和声明array一样,只是少了长度,此为空(nil)切片
    s2 := []int{}
 
    //make([]T, length, capacity) //capacity省略,则和length的值相同
    var s3 []int = make([]int, 0)
    s4 := make([]int, 0, 0)
 
    s5 := []int{1, 2, 3} //创建切片并初始化


同时slice可以理解为java中的arraylist,是一个可以扩容且传递地址值的数据类型。

下面是具体的例子:

func test(s []int) { //切片做函数参数
    s[0] = -1
    fmt.Println("test : ")
    for i, v := range s {
        fmt.Printf("s[%d]=%d, ", i, v)
        //s[0]=-1, s[1]=1, s[2]=2, s[3]=3, s[4]=4, s[5]=5, s[6]=6, s[7]=7, s[8]=8, s[9]=9,
    }
    fmt.Println("\n")
}
 
func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    test(slice)
 
    fmt.Println("main : ")
    for i, v := range slice {
        fmt.Printf("slice[%d]=%d, ", i, v)
        //slice[0]=-1, slice[1]=1, slice[2]=2, slice[3]=3, slice[4]=4, slice[5]=5, slice[6]=6, slice[7]=7, slice[8]=8, slice[9]=9,
    }
    fmt.Println("\n")
}


slice的取值操作和数组一样,通过索引下标取值,这里不做更多的叙述。

三、pointer

Go语言虽然保留了指针,但与其它编程语言不同的是:

l 默认值 nil,没有 NULL 常量

l 操作符 “&” 取变量地址, “*” 通过指针访问目标对象

l 不支持指针运算,不支持 “->” 运算符,直接⽤ “.” 访问目标成员

func main() {
    var a int = 10              //声明一个变量,同时初始化
    fmt.Printf("&a = %p\n", &a) //操作符 "&" 取变量地址
 
    var p *int = nil //声明一个变量p, 类型为 *int, 指针类型
    p = &a
    fmt.Printf("p = %p\n", p)
    fmt.Printf("a = %d, *p = %d\n", a, *p)
 
    *p = 111 //*p操作指针所指向的内存,即为a
    fmt.Printf("a = %d, *p = %d\n", a, *p)
}


值得注意的是:指针在初始化后需要分配内存空间。这也是c语言中经常强调的一点,如果不通过malloc分配内存空间直接使用指针则会报错,go语言中也一样,需要使用特定的函数进行内存分配。

在go语言中,使用new()函数实现对指针的内存分配:

func main() {
    var p1 *int
    p1 = new(int)              //p1为*int 类型, 指向匿名的int变量
    fmt.Println("*p1 = ", *p1) //*p1 =  0
 
    p2 := new(int) //p2为*int 类型, 指向匿名的int变量
    *p2 = 111
    fmt.Println("*p2 = ", *p2) //*p1 =  111
}

指针的其他操作和c语言差不多,就不做更多的总结。

四、map

map的概念大家一定知道,Go语言中的map(映射、字典)是一种内置的数据结构,它是一个无序的key—value对的集合

在一个map里所有的键都是唯一的,而且必须是支持==和!=操作符的类型,切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。map值可以是任意类型,没有限制。map里所有键的数据类型必须是相同的,值也必须如何,但键和值的数据类型可以不相同。

map的创建:

    var m1 map[int]string  //只是声明一个map,没有初始化, 此为空(nil)map
    fmt.Println(m1 == nil) //true
    //m1[1] = "mike" //err, panic: assignment to entry in nil map
 
    //m2, m3的创建方法是等价的
    m2 := map[int]string{}
    m3 := make(map[int]string)
    fmt.Println(m2, m3) //map[] map[]
 
    m4 := make(map[int]string, 10) //第2个参数指定容量
    fmt.Println(m4)                //map[]

map的初始化:

    //1、定义同时初始化
    var m1 map[int]string = map[int]string{1: "mike", 2: "yoyo"}
    fmt.Println(m1) //map[1:mike 2:yoyo]
 
    //2、自动推导类型 :=
    m2 := map[int]string{1: "mike", 2: "yoyo"}
    fmt.Println(m2)

map通过delete函数来删除map内的键值对:

    m1 := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}
    //迭代遍历1,第一个返回值是key,第二个返回值是value
    for k, v := range m1 {
        fmt.Printf("%d ----> %s\n", k, v)
        //1 ----> mike
        //2 ----> yoyo
        //3 ----> lily
    }
 
    delete(m1, 2) //删除key值为3的map
 
    for k, v := range m1 {
        fmt.Printf("%d ----> %s\n", k, v)
        //1 ----> mike
        //3 ----> lily
    }

通过range可实现map的遍历:

    m1 := map[int]string{1: "mike", 2: "yoyo"}
    //迭代遍历1,第一个返回值是key,第二个返回值是value
    for k, v := range m1 {
        fmt.Printf("%d ----> %s\n", k, v)
        //1 ----> mike
        //2 ----> yoyo
    }
 
    //迭代遍历2,第一个返回值是key,第二个返回值是value(可省略)
    for k := range m1 {
        fmt.Printf("%d ----> %s\n", k, m1[k])
        //1 ----> mike
        //2 ----> yoyo
    }
 
    //判断某个key所对应的value是否存在, 第一个返回值是value(如果存在的话)
    value, ok := m1[1]
    fmt.Println("value = ", value, ", ok = ", ok) //value =  mike , ok =  true
 
    value2, ok2 := m1[3]
    fmt.Println("value2 = ", value2, ", ok2 = ", ok2) //value2 =   , ok2 =  false

值得注意的是:在函数间传递映射并不会制造出该映射的一个副本,不是值传递,而是引用传递:

func DeleteMap(m map[int]string, key int) {
    delete(m, key) //删除key值为3的map
 
    for k, v := range m {
        fmt.Printf("len(m)=%d, %d ----> %s\n", len(m), k, v)
        //len(m)=2, 1 ----> mike
        //len(m)=2, 3 ----> lily
    }
}
 
func main() {
    m := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}
 
    DeleteMap(m, 2) //删除key值为3的map
 
    for k, v := range m {
        fmt.Printf("len(m)=%d, %d ----> %s\n", len(m), k, v)
        //len(m)=2, 1 ----> mike
        //len(m)=2, 3 ----> lily
    }
}

五、struct

有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。

结构体是一种聚合的数据类型,它是由一系列具有相同类型或不同类型的数据构成的数据集合。每个数据称为结构体的成员。

struct的初始化:

type Student struct {
    id   int
    name string
    sex  byte
    age  int
    addr string
}
 
func main() {
    //1、顺序初始化,必须每个成员都初始化
    var s1 Student = Student{1, "mike", 'm', 18, "sz"}
    s2 := Student{2, "yoyo", 'f', 20, "sz"}
    //s3 := Student{2, "tom", 'm', 20} //err, too few values in struct initializer
 
    //2、指定初始化某个成员,没有初始化的成员为零值
    s4 := Student{id: 2, name: "lily"}
}

值得注意的是:struct作为参数传递时比较特殊,如果只是struct作为参数,则是值传递,在函数内部无法改变struct内部的值:

func printStudentValue(tmp Student) {
    tmp.id = 250
    //printStudentValue tmp =  {250 mike 109 18 sz}
    fmt.Println("printStudentValue tmp = ", tmp)
}
 
func main() {
var s Student = Student{1, "mike", 'm', 18, "sz"}
 
    printStudentValue(s)        //值传递,形参的修改不会影响到实参
    fmt.Println("main s = ", s) //main s =  {1 mike 109 18 sz}
}

但是如果是struct的指针时,则是引用传递,可以修改struct内部的值:

func printStudentPointer(p *Student) {
    p.id = 250
    //printStudentPointer p =  &{250 mike 109 18 sz}
    fmt.Println("printStudentPointer p = ", p)
}
 
func main() {
    var s Student = Student{1, "mike", 'm', 18, "sz"}
 
    printStudentPointer(&s)     //引用(地址)传递,形参的修改会影响到实参
    fmt.Println("main s = ", s) //main s =  {250 mike 109 18 sz}
}