Go 语言入门基础语法

Posted bylight

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 语言入门基础语法相关的知识,希望对你有一定的参考价值。

写在前面

在学习 Go 语言之前,我自己是有一定的 Java 和 C++ 基础的,这篇文章主要是基于A tour of Go编写的,主要是希望记录一下自己的学习历程,加深自己的理解

Go 语言入门(一)基础语法

本地安装 Go 语言环境

关于如何安装 Go 语言的编程环境,我推荐大家看这篇文章

编写 Hello, World

学习语言时,首先编写一个「Hello, World」已经成了程序员的习惯,这里我们也编写一个,顺便测试一下 Go语言环境是否搭建成功了:

首先创建一个名为hello.go的文件,编写代码如下:

package main

import "fmt"

func main() {
    fmt.Printf("Hello, World
")
}

接着我们在命令行中使用 go 工具运行它:

$ go run hello.go
Hello, World

如果像上面一样看到了「Hello, World」信息,则表示我们已经迈出学习 Go 语言的第一步了。

关于更多 Go 语言的编程方法已经相应命令,我们可以看这里,它为我们清楚地介绍了 Go 语言的环境变量等相关设置。

在都搭建完成之后,我们就可以进入 Go 语言的语法学习了。

包、变量和函数

学习 Go 语言语法之前,我们要知道他是在「C语言」的基础之上发展的,所以他们之间很多语法都是相似的。

声明包和导入包

每个 Go 语言都是由「包」构成的,所有的 Go 程序都是从main包开始运行的;同时,他也和 Java 中一样,需要一个main函数作为「程序入口」。

// 声明当前程序为 main 包
package main

我们使用import语句来导入我们需要的包,我们可以一次 import 导入一个包,也可以一次 import 同时导入多个包:

// 导入一个包
import "fmt"
import "math/rand"

// 同时导入多个包
import (
    "fmt"
    "math/rand"
)

需要注意的是,「包名」和「导入路径的最后一个元素」一定要是一致的。例如前文导入的import "math/rand",它的对应源码中,第一行的包声明应该是package rand

导出(getter)

在 Go 中,如果一个名字以大写字母开头,那么他就是已导出的(可以理解为 Java 中的 getter)。

例如下述代码中,我们导出「math」包中的Pi变量并打印:

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.Pi)
}

从上面我们可以看出,虽然 Go 是发展自 C语言,但 Go 语言语句结尾不加分号

同时,Go 语言中的输出语句和「Java」中很类似:

Go 语言中导入了fmt包,引用它的Println函数,和 Java 中一样,这个输出语句会在输出结束之后自动换行,但 Go 中语句拼接是使用,而不是+,例如下面的语句:

const World = "世界"
fmt.Println("Hello", World)

同样的,我们还可以使用fmt.Print()fmt.Printf()来打印我们想要的信息,后两者使用方法都是和 Java 类似的。

Printf() 中的变量通配符

Go 中Printf()的通配符和其他语言中有一些不同,下面我们给出两个常用的:

  • %v,value,也就是变量的值

  • %T,type,打印变量的类型

更多的通配符,可以看这里

变量

在接触函数之前,我们先看看 Go 语言中变量的相关知识

声明变量及其初始化

下面我们声明三个全局bool类型变量,再声明局部int类型变量,并对部分变量进行赋值,并打印出它们:

package main

import "fmt"

var c, python, java bool

func main() {
    var i int
    var num int = 50
    var num1, num2 int = 51, 52
    fmt.Println(i, c, python, java)
    fmt.Println(num, num1, num2)
}

运行后,结果如下:

0 false false false
50 51 52

从上面示例代码中,我们可以知道如下情况:

  • go 语言中声明变量时,以var 变量名 变量类型的形式进行,我们可以在一个语句中同时声明多个变量

  • go 语言中变量的声明包含默认的初始值,我们也可以在声明的同时为他赋予初始值:var num int = 50

关于为什么使用var num int而不是int num,是因为语言发明者认为这样可以让 Go 的语句不容易出现歧义,增加程序的可读性。有兴趣的可以查看这篇关于 Go 语法声明的文章进行详细的了解。

短变量声明和类型推导

函数中,我们可以使用简洁赋值语句:=在类型明确的地方代替var声明,Go 能够自动确定合适的类型并帮我们完成变量的声明,我们将这种行为称为「类型推导」:

func main() {
    // 只能用于函数中,也就是不能用于声明全局变量
    k := 3
}

因为存在「类型推导」,所以在有初始值的情况下,我们可以省略变量的类型:

package main

import "fmt"

var v = 1

func main() {
    fmt.Printf("v=%v is of type %T
", v, v)
}

它的输出结果如下:

v=1 is of type int

变量的基本类型

Go 之中的变量基本类型如下:

bool

string

int  int8  int16  int32  int64          // 带数字的为指定位宽的 int
uint uint8 uint16 uint32 uint64 uintptr // 上面 int 的无符号形式

byte // uint8 的别名

rune // int32 的别名,表示 Unicode 的一个码点

float32 float64 // 指定位宽的浮点数

complex64 complex128 // 指定位宽的复数

上面便是 Go 中的基本类型,对于int``uintuinptr,它们在 32 位系统上位 32 位宽,64 位系统上为 64 位宽。没有特定要求的情况下,如果我们需要声明一个整形变量,只需要直接使用int就好了。

对于基本变量类型,它们默认值的情况如下:

  • 数值类型,默认为0,包括intfloat

  • 布尔类型,默认为false

  • 字符串类型,默认为""(空字符串)

同时,我们还要注意,Go 中没有char类型的变量,所以类似v := ‘a‘这样的语句,v 的类型将会是int32

示例代码:

package main

import "fmt"

func main() {
    // 类型初始值
    var i int
    var f float64
    var b bool
    var s string
    fmt.Printf("%v %v %v %q
", i, f, b, s)
    // 注意 Go 中不存在 char 类型
    v := 'a'
    fmt.Printf("v=%v is of type %T
", v, v)
}

运行结果如下:

0 0 false ""
v=97 is of type int32

强制类型转换

Go 语言中,我们使用表达式T(v)将值v转换为类型T

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

如果是在函数中,我们也可以使用短变量声明的形式:

i := 42
f := float64(i)
u := uint(f)

但需要注意的是,和其他语言不同,Go 在不同类型的项之间赋值时,必须显式转换,它并不会对变量进行自动转型。

常量

常量的声明和变量类似,不过我们使用const关键字来代替var,常量可以为全局或者变量,但是他不能使用:=语法声明。

// 这里类型推导能够为 Pi 自动选择 folat64 的类型
const Pi = 3.14

函数

Go 中的函数需要用func显式声明,其中的行参和之前的变量声明一样,类型是在参数名之后的,这也意味着我们可以从变量的角度来看待和使用函数;类似的,函数的返回值也在整个函数的声明之后,我们用下面的例子可以直观地理解:

package main

import "fmt"

func main() {
    fmt.Println(add(42, 13))
}

func add(a int, b int) int {
    return a + b
}

同时,从上面我们还可以看出,不同于 C 语言需要在使用函数之前显式声明或者定义函数,Go 可以在任意地方(可以在使用之前,也可以在使用之后)定义函数。

在 Go 之中,如果当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

例如上面的add()函数:

func add(a, b int) int {
    return a + b
}

多值返回

Go 语言中,函数可以返回任意数量的返回值,例如下面的swap()函数返回两个字符串:

func swap(a, b string) (string, string) {
    return b, a
}

func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

上面的程序运行后应该是输出word hello

为函数的返回值命名

Go 之中函数的返回值可被命名,它们会被视作定义在函数顶部的变量。

下面的例子中,我们为函数的返回值命名为xy

func split(sum int) (x, y int) {
    x = sum * 4
    y = sum - 1
    // 这里会返回 x 和 y
    return
}

上面的函数中,我们在函数顶部定义了int类型的变量xy,并在函数体中对他们进行赋值,最后的return语句则将xy的值直接返回。

在实际使用这种命名的返回值时,应要遵循下面两条约定:

  • 返回值的名称应当具有一定的意义,它可以作为文档使用;

  • 「直接返回语句」应当仅用在前面这样的短函数中,在长的函数中它们会影响代码的可读性。

函数,一种特殊的变量

前面提到,我们可以从变量的角度来看待和使用函数,那么,作为一种特殊的变量,函数自身也可以作为「参数」或者「返回值」在函数中出现:

用例子可以很好地说明:

// 传入了一个「返回值为 float64 的函数」作为参数
func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    // 将 hypot 函数作为参数传入
    fmt.Println(compute(hypot))
    // 将 Pow 函数作为参数传入
    fmt.Println(compute(math.Pow))
}

函数的闭包

「闭包」这个特性有一些抽象,我们用例子说明会比较好理解:

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    // 获取两个函数
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println("i=", i)
        fmt.Println(
            "neg(-2*i)=", neg(-2*i),
            "pos(i)=", pos(i),
        )
    }
}

在上面的代码中,adder()函数的返回了一个「返回值为 int 的函数」,我们能够发现,函数func(int)会使用它函数体之外的参数sum,并对sum进行计算并修改,然后返回。那么,在 Go 语言中,每次调用adder()所返回的函数时,函数对 sum 的修改将会保存

我们先看看上面代码的执行结果:

i= 0
neg(-2*i)= 0 pos(i)= 0
i= 1
neg(-2*i)= -2 pos(i)= 1
i= 2
neg(-2*i)= -6 pos(i)= 3
i= 3
neg(-2*i)= -12 pos(i)= 6
i= 4
neg(-2*i)= -20 pos(i)= 10
i= 5
neg(-2*i)= -30 pos(i)= 15
i= 6
neg(-2*i)= -42 pos(i)= 21
i= 7
neg(-2*i)= -56 pos(i)= 28
i= 8
neg(-2*i)= -72 pos(i)= 36
i= 9
neg(-2*i)= -90 pos(i)= 45

可以看到,在i=2时,pos(2)的返回值为 3,因为在执行完pos(1)后,sum 的值已经变成 1 了,所以在pos(2)中,sum += 2实际上是sum = 1 + 2。同样地,在neg()中,它也有一个自己独立的「sum」。

也就是,在「pos」和「neg」函数被初始化的时候,「sum」就相当于成为了它们两个函数的静态变量一样,每次对它进行的赋值都会保存。像「pos」和「neg」这两个函数,它们就是adder()返回的两个闭包,它们各自都有一个绑定的变量「sum」。

流程控制语句:for、if、else、switch 和 defer

for 语句

Go 之中只有一种循环结构:for 循环。

基本的「for循环」和 Java、C 等语言相似:它们都是由三部分组成,每个部分之中用分号;隔开:

  • 初始化语句:在第一次迭代前执行

  • 条件表达式:在每次迭代前求值

  • 后置语句:在每次迭代的结尾执行

但Go 的 for 语句后面的三个构成部分外没有小括号, 大括号{ }则是必须的。所以正确的 for循环 是下面这样的:

// 打印 0 ~ 9 这十个数字
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

在 Go 之中,我们可以将 for 的三个部分都省略,这样就变成了下面这个样子:

sum := 1
for ; ; {
    sum += sum
    if sum > 1000 {
        break
    }
}

不难发现,这其实就是一个while(true)循环,此时,我们可以去掉分号,也就是能够用for实现while语句;那么,上面的while(true)我们可以这样更加简洁地表示:

sum := 1
for {
    sum += sum
    if sum > 1000 {
        break
    }
}
  • for之后可以只保留「条件表达式」部分,也就是while的形式
// for 就是 go 语言中的 while 循环
sum := 1
for sum <= 1000 {
    sum += sum
}

if、else 语句

其实「if语句」在之前的 for 循环中我们已经使用过了。它和 for 的语法一样:

  • 表达式外无需小括号( )

  • 大括号{ }是必须的。

不过,Go 之中的 if 有一个特殊的语法:可以在条件表达式前执行一个简单的初始化语句。

这种情况下初始化的变量的作用域仅在if、else语句之中

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    } else {
        fmt.Printf("%g >= %g
", v, lim)
    }
    // 这里就不能使用 v 了
    // return v 就会报错

    return lim
}

switch 语句

「switch 语句」在 C,Java 中均有出现,Go 中的 switch 和它们主要有以下两点不同:

  • Go 的每个case语句后已经隐式地加好了break语句,因此 Go 只会运行选定的 case

  • Go 中switchcase无需为常量,且取值不必为整数。

下面的示例程序可以很好地帮助我们理解这两条特性:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("When's Saturday?")
    today := time.Now().Weekday()

    switch time.Saturday {
    // 这里每个 case 都是一个表达式,且值不是整数
    case today + 0:
        fmt.Println("Today.")
        // 在 case 后已经隐式的存在 break 了
    case today + 1:
        fmt.Println("Tomorrow.")
    case today + 2:
        fmt.Println("In two days.")
    default:
        fmt.Println("Too far away.")
    }
}

不带条件的 switch 语句

没有条件的 switch 同 switch true 一样。

这种形式能将一长串if-then-else写得更加清晰:

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

defer 语句

「defer 语句」能让标示的语句延迟执行:它会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

例如下面的示例程序:

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

运行后,我们得到的打印结果是这样的:

hello
world

也就是在main()函数执行完毕之后才打印了 world。

defer 栈

如果一个函数中有多个defer语句,那么它们会按照执行顺序被压入一个栈中,在外层函数返回之后,所有 defer 语句会按照后进先出的顺序被调用。

所以下面的程序应该是在 main() 函数返回后,从10到1顺序打印:

func main() {
    fmt.Println("counting")

    // defer 语句将依次入栈
    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}

我们可以从这篇博文中获取更多关于defer的信息。

结构体、数组、切片和映射

在学习这一部分的语法之前,我们需要先了解 Go 语言之中的「指针」。

指针

和 C 类似的,Go 中也拥有指针,它保存了变量的内存地址,Go 之中将nil作为空指针

当我们需要声明指针时,*T即标示T类型的指针:

// 这里声明了一个 int 类型的指针
var p *int

类似于 C 的,取地址符&会生成一个指向该变量地址的指针:

i := 42
// 这样指针 p 就指向了 i
p = &i

如果我们需要获取指针指向变量的值,我们便需要使用*操作符来读取对应的变量值:

// 打印结果为 42
fmt.Println(*p)
// 等价于 i = 21
*p = 21
// 打印结果为 21
fmt.Println(i)

那么,我们总结一下,就能归纳出指针的用法了:

  • *T:声明特定类型T的指针,var p = *int

  • &var:获取指向变量var的指针,p = &i

  • *p:获取指针p所指向变量的值,*p = 21

可以看出,Go 的指针语法和 C 几乎一样,但有一点与 C 不同,Go 没有指针运算。

结构体

定义结构体

结构体在 C 语言中也有相应的定义,实际上,一个结构体struct就是一组字段field。我们使用关键字type而不是var来定义它,结构体的类型是struct,因此,一个结构体的定义应该是这样的:

type Vertex struct {
    X int
    Y int
}

创建一个结构体

Go 语言中,我们可以通过以下方式来创建一个已经定义过的结构体:

  • 通过直接列出字段的值来新分配一个结构体

    例如v1 = Vertex{1, 2},它创建一个 Vertex 类型的结构体,结构体的 X=1、Y=2

  • 使用Name:语法可以仅列出部分字段,没有被列出的字段则被赋予默认值

    v2 = Vertex{X: 1},Y:0 被隐式地赋予
    v3 = Vertex{}, X:0 Y:0

指向结构体的指针

对于一个已创建的结构体,我们应该将其看作一个变量,所以我们可以使用取地址符&来获取指向它的指针:

p = &Vertex{}

访问结构体的字段

在上面的结构体「Vertex」中,我们声明了 X 和 Y 两个 int 类型的字段,和 C 中一样,我们可以通过点号.来访问结构体中的字段:

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    // 初始化一个 X = 1, Y = 2 的结构体
    v := Vertex{1, 2}
    v.X = 4
    // 打印结构为 {4 2}
    fmt.Println(v)
    
    // 结构体指针
    p := &v
    p.X = 1e9
    // 打印结构为 {1000000000 2}
    fmt.Println(v)
}

上面的例子中,我们可以看到:对于指向结构体的指针,和 C 语言中相同,我们可以使用(*p).X来访问结构体中的 X 字段,而 Go 中为了简化代码,所以我们可以直接使用p.X来访问 X 字段。

数组和切片

Go 中定义数组的语法比较特殊,使用类型[n]T表示一个含 n 个元素的,类型为 T 的数组。

// 定义一个长度为 10 的 int 数组
var nums [10]int
// 定义一个长度为 6 的数组并为其赋值
primes := [6]int{2, 3, 5, 7, 11, 13}

Go 中的数组长度是它类型的一部分,因此一旦定义,长度便固定了。但 Go 中为我们提供了一种便利的方式来灵活地使用数组:「切片」

什么是切片

「切片」本质上是数组的引用,它描述了底层数组中的一段。因此更改切片的元素会修改其底层数组中对应的元素,与它共享底层数组的切片都会观测到这些修改。

切片相当于为我们提供了使用动态大小数组的方法,在实践中,切片比数组更加常用。

如何使用

我们可以将「切片」看作一种特殊的元素,它的类型符是[]T;而一个数组的切片由两个下标来界定,分别代表上界和下界,两者以冒号分割a[low:high],需要注意的是,和大多数编程语言一样,切片的上下界是左闭右开的。

因此,如果我们需要定义一个对数组 nums 的切片,其中包含下标 1~3 的元素,那么我们应该这样做:

// 需要注意是左闭右开
var part []int = nums[1:4]

我们也可以直接创建一个切片,我们在初始化切片时,可以给他任意个数的元素,就像一个不定长的数组:

这是初始化一个数组的语法:

[3]bool{true, true, false}

这是初始化切片的文法,本质上,Go 会创建一个和上面相同的数组,然后构建一个引用了它的切片

[]bool{true, true, false}

访问切片元素时,我们直接用下标访问切片就好了part[0]

我们也可以像使用多维数组一样,创建切片的切片:

package main

import (
    "fmt"
    "strings"
)

func main() {
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }
    for i := 0; i < len(board); i++ {
        fmt.Printf("%s
", strings.Join(board[i], " "))
    }
    
    fmt.Println()
    fmt.Println("==更改元素==")
    fmt.Println()

    // 更改切片元素
    board[0][0] = "X"
    board[0][2] = "X"
    board[1][1] = "X"
    board[2][0] = "X"
    board[2][2] = "X"
    for i := 0; i < len(board); i++ {
        fmt.Printf("%s
", strings.Join(board[i], " "))
    }
}

运行结果如下:

_ _ _
_ _ _
_ _ _

==更改元素==

X _ X
_ X _
X _ X

切片的默认行为

在创建切片的时候,如果我们没有明确给定上界或下界,那么切片会为其赋予默认值:

  • 「默认下界」为0

  • 「默认上界」为数组长度

所以,对于数组var nums [10]int来说,下面的四个切片是等价的:

a[0:10]
a[:10]
a[0:]
a[:]

切片的两个属性:「长度」和「容量」

和数组只拥有「长度length」这一属性不同,切片拥有长度length容量capacity这两个属性:

  • 「长度」:切片当前的元素个数;使用len(a)可获取切片长度

  • 「容量」:从切片第一个元素开始,到底层数组末尾元素的个数;使用cap(a)可获取切片容量。

前面提到,切片的本质实际上是底层数组的一段引用,由此便能很好的理解「容量」的概念了;同时,如果切片长度超过了底层数组的长度,那么自然会产生数组越界

下面我们用一个例子说明:

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // 截取切片使其长度为 0
    s = s[:0]
    printSlice(s)

    // 拓展其长度
    s = s[:4]
    printSlice(s)

    // 舍弃前两个值
    // 注意,这里容量会减小
    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v
", len(s), cap(s), s)
}

运行结果如下:

// 从 0 开始切片,容量为数组长度 6
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
// 这里因为切片下界是2,因此容量为 6-2=4
len=2 cap=4 [5 7]

特别地,如果切片并没有指向任何的底层数组,那么它是一个空的切片,用nil表示,它的长度和容量都为 0。

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

运行结果:

[] 0 0
nil!

使用「make()」方法快捷地创建切片

我们可以使用内建函数make()来快捷地创建切片:

a := make([]int, 5)  // len(a)=5

它实际上做了两件事:

  • 新建一个元素为默认值的数组

  • 返回一个引用了该数组的切片

所以如果我们打印上面的切片 a 的长度、容量等信息,我们可以得到以下结果:

a len=5 cap=5 [0 0 0 0 0]

实际上,make()函数拥有三个参数,分别代表底层数组的元素类型、切片长度、切片容量:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

为切片追加元素

前面我们评价切片时,说到它是动态使用数组的方法;但切片本质上是指向底层数组的一个引用,我们可以通过[low:high]来获取当前切片的子集,以获取更小的数组;但使用上下界获取的切片长度和容量都不能超过底层数组的长度,不能获取更长的数组,那我们如何让切片满足「动态数组」这一使用要求呢?

为了满足获取更长数组的要求,Go 语言为我们提供了append()函数,它能够为切片追加新的元素,内建函数的文档对此函数有详细的介绍。

func append(s []T, vs ...T) []T

append()的第一个参数s是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。它会返回一个包含原切片所有元素加上新添加元素的切片,根据底层数组长度和目标切片长度,会有下面两种情况:

  • 底层数组长度满足目标切片长度要求:相当于对底层数组相应索引的元素重新赋值,再让切片长度增加。

  • 底层数组长度过短,不满足目标切片长度要求:分配一个新的、更大的数组,再让切片指向这个新数组(之后对切片做的修改也不会影响到原数组了)。

下面我们用一个例子来说明:

package main

import "fmt"

func main() {
    // 原数组为 [1, 2, 3]
    var nums [3]int = [3]int{1, 2, 3}

    // 此时切片 s 指向数组 nums,为 [1]
    s := nums[:1]
    // 1. 因为原数组长度满足 append 后的要求长度,这里会对原数组也产生修改
    s = append(s, 0)
    fmt.Println("nums: ", nums)
    fmt.Println("s: ", s)
    // --输出如下--
    // nums: [1, 0, 3]
    // s: [1, 0]

    // 2. 这里原数组的长度明显过短,满足不了目标切片要求
    // append 会分配一个新的数组,让 s 指向新数组
    s = append(s, 51, 52, 53, 55)
    fmt.Println("nums: ", nums)
    fmt.Println("s: ", s)
    // --输出如下--
    // nums: [1, 0, 3]
    // s: [1 0 51 52 53 55]

    // 3. 这里让 s 的长度缩小至 1,也就是现在是 [1]
    s = s[:1]
    // 因为 s 已经指向新数组而不是 nums 了,所以修改不会影响 nums
    s = append(s, -1)
    fmt.Println("nums: ", nums)
    fmt.Println("s: ", s)
    // --输出如下--
    // nums: [1, 0, 3]
    // s: [1, -1]
}

更多关于切片的内容,可以查看Go 切片:用法和本质进行深入的理解。

使用「for-range」来遍历数组或切片

Go 中我们可以使用「for-range」的形式来遍历数组或者切片:

func main() {
    nums := []int{0, 1, 2, 3}
    for i, v := range nums {
        fmt.Println("index=", i, ", value=", v)
    }
}

每次range函数会返回两个值,第一个值为「当前元素下标」,第二个值为「当前元素值」,我们可以使用_来忽略它们:

for i, _ := range nums 
for _, v := range nums

特别地,如果我们只需要索引,那么我们可以只保留第一个变量:

for i := range nums

映射(键值对)

映射,我习惯将其称为键值对,也就是在一个映射中维护键key和对应值value的唯一映射关系的数据结构,我们使用map[key]value语法来声明它:

var m map[string]int

在仅声明的情况下,映射是一个空指针nil,即是一个空的引用,我们可以使用make函数来对它进行初始化:

m = make(map[string]int)
// 初始化后就可以赋值了
m["a"] = 1

或是在声明时同时对它进行初始化:

var m map[string]int = map[string]int{
    "a" : 1,
    // 因为这里右括号 } 在下一行,所以这里行尾需要逗号
    "b" : 2, 
}

// 所以这样最后没有逗号,也是可以的
var m map[string]int = map[string]int{"a" : 1, "b" : 2}

访问、修改映射

1.在映射m插入元素

m[key] = elem

2.获取元素

elem = m[key]

3.删除元素

delete(m, key)

4.检测元素是否存在

这里我们需要使用「双赋值」:

elem, ok = m[key]
  • 如果key存在,「ok」为true;「elem」为 key 对应的元素值

  • 如果key不存在,「ok」为false;「elem」为其类型的默认值

如果「elem」和「ok」未声明,我们可以使用「短变量声明」:

elem, ok := m[key]

以上是关于Go 语言入门基础语法的主要内容,如果未能解决你的问题,请参考以下文章

Go入门教程语法基础

GO语言(十六):模糊测试入门(上)

Go语言入门篇-命令 与 语法

Go语言基础语法(一)

0基础学Go语言光速入门教程

Golang入门:一天学完GO的进阶语法