go的入门快速上手(2) | 青训营

75 阅读10分钟

go的基础语法

Hello,World!

先从最经典的hello,world来看看go代码的结构

package main  // 声明 main 包,表明当前是一个可执行程序

import "fmt"  // 导入内置 fmt 

func main(){  // main函数,是程序执行的入口
    fmt.Println("Hello World!")  // 在终端打印 Hello World!
}

首先第一行Go语言的代码通过(package)组织,包类似于其它语言里的库(libraries)或者模块(modules)。一个包由位于单个目录下的一个或多个.go源代码文件组成,目录定义包的作用。每个源文件都以一条package声明语句开始,这个例子里就是package main,表示该文件属于哪个包,紧跟着一系列导入(import)的包,之后是存储在这个文件里的程序语句。

Go的标准库提供了100多个包,以支持常见功能,如输入、输出、排序以及文本处理。比如fmt包,就含有格式化输出、接收输入的函数。Println是其中一个基础函数,可以打印以空格间隔的一个或多个值,并在最后添加一个换行符,从而输出一整行。

就比如代码第二行,使用了fmt包,在后续代码中使用fmt包中的函数,来输出Hello,World!,而为了声明这些包,就必须在这些包之前使用import来声明。

main包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在main里的main 函数 也很特殊,它是整个程序执行时的入口(译注:C系语言差不多都这样)。main函数所做的事情就是程序做的。当然了,main函数一般调用其它包里的函数完成很多工作(如:fmt.Println)。

变量和常量

变量

go是一门强类型语言,每一个变量都有他对应的变量类型,和其他语言一样,有floatboolstring等等类型。

go语言的变量声明格式:

    var 变量名 变量类型

举个例子:

    var name string
    var age int
    var isOk bool

也可以批量声明,避免过于反复的操作:

 var (
        a string
        b int
        c bool
        d float32
    )

go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

    var 变量名 类型 = 表达式

举个例子:

    var name string = "pprof.cn"
    var sex int = 1

或者一次初始化多个变量:

    var name, sex = "pprof.cn", 1

也可以将类型省略,go语言会自行推导:

    var name = "pprof.cn"
    var sex = 1

还有一种更为简单更为常见的声明方法:

变量名 := 值

举个例子:

package main

import (
    "fmt"
)
// 全局变量m
var m = 100

func main() {
    n := 10
    m := 200 // 此处声明局部变量m
    fmt.Println(m, n)
}

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:

func foo() (int, string) {
    return 10, "Q1mi"
}
func main() {
    x, _ := foo()
    _, y := foo()
    fmt.Println("x=", x)
    fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const,常量在定义的时候必须赋值

    const pi = 3.1415
    const e = 2.7182

数组(Array)与切片(Slice)

数组(Array)

Golang Array和以往认知的数组有很大不同。

    1. 数组:是同一种数据类型的固定长度的序列。
    2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
    3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
    4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
    5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
    6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
    7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
    8.指针数组 [n]*T,数组指针 *[n]T。

因为数组的长度是固定的,不方便扩容,在go中我们更常用切片(Slice)来替代数组。这里我就列举一下一维、多维数组怎样声明。接着就详细介绍切片了。

一维数组:

    全局:
    var arr0 [5]int = [5]int{1, 2, 3}
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var arr2 = [...]int{1, 2, 3, 4, 5, 6}
    var str = [5]string{3: "hello world", 4: "tom"}
    局部:
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }

多维数组:

    全局
    var arr0 [5][3]int
    var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
    局部:
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

切片(Slice)

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。 需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

    1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
    2. 切片的长度可以改变,因此,切片是一个可变的数组。
    3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。 
    4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
    5. 切片的定义:var 变量名 []类型,比如 var str []string  var arr []int6. 如果 slice == nil,那么 lencap 结果都等于 0

切片的各种声明方式:

package main

import "fmt"

func main() {
   //1.声明切片
   var s1 []int
   if s1 == nil {
      fmt.Println("是空")
   } else {
      fmt.Println("不是空")
   }
   // 2.:=
   s2 := []int{}
   // 3.make()
   var s3 []int = make([]int, 0)
   fmt.Println(s1, s2, s3)
   // 4.初始化赋值
   var s4 []int = make([]int, 0, 0)
   fmt.Println(s4)
   s5 := []int{1, 2, 3}
   fmt.Println(s5)
   // 5.从数组切片
   arr := [5]int{1, 2, 3, 4, 5}
   var s6 []int
   // 前包后不包
   s6 = arr[1:4]
   fmt.Println(s6)
}

切片还可以通过append函数来扩容:

package main

import (
    "fmt"
)

func main() {

    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a)
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b)
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c)
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d)
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e)

}

Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。 Go语言中 map的定义语法如下:

    map[KeyType]ValueType

其中,

    KeyType:表示键的类型。

    ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

    make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

Map的基本使用

map中的数据都是成对出现的,map的基本使用示例代码如下:

func main() {
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
}

输出:

    map[小明:100 张三:90]
    100
    type of a:map[string]int

map也支持在声明的时候填充元素,例如:

func main() {
    userInfo := map[string]string{
        "username": "pprof.cn",
        "password": "123456",
    }
    fmt.Println(userInfo) //
}

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

    value, ok := map[key]

Go语言中使用for range遍历map。

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}

但我们只想遍历key的时候,可以按下面的写法:

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k := range scoreMap {
        fmt.Println(k)
    }
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:

    delete(map, key)

结构体

结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

    type 类型名 struct {
        字段名 字段类型
        字段名 字段类型
        …
    }

其中:

    1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
    2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
    3.字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

    type person struct {
        name string
        city string
        age  int8
    }

同样类型的字段也可以写在一行,

    type person1 struct {
        name, city string
        age        int8
    }

这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。

自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。 自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

    //将MyInt定义为int类型
    type MyInt int

通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

我们也可以为其添加类别名

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

    type TypeAlias = Type

我们之前见过的rune和byte就是类型别名,他们的定义如下:

    type byte = uint8
    type rune = int32

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

    var 结构体实例 结构体类型

举个例子:

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p1 person
    p1.name = "pprof.cn"
    p1.city = "北京"
    p1.age = 18
    //两种输出方法
    fmt.Printf("p1=%v\n", p1)  //p1={pprof.cn 北京 18}
    fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", city:"北京", age:18}
}

我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。