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``uint
和uinptr
,它们在 32 位系统上位 32 位宽,64 位系统上为 64 位宽。没有特定要求的情况下,如果我们需要声明一个整形变量,只需要直接使用int
就好了。
对于基本变量类型,它们默认值的情况如下:
数值类型,默认为
0
,包括int
和float
布尔类型,默认为
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 之中函数的返回值可被命名,它们会被视作定义在函数顶部的变量。
下面的例子中,我们为函数的返回值命名为x
和y
:
func split(sum int) (x, y int) {
x = sum * 4
y = sum - 1
// 这里会返回 x 和 y
return
}
上面的函数中,我们在函数顶部定义了int
类型的变量x
和y
,并在函数体中对他们进行赋值,最后的return
语句则将x
和y
的值直接返回。
在实际使用这种命名的返回值时,应要遵循下面两条约定:
返回值的名称应当具有一定的意义,它可以作为文档使用;
「直接返回语句」应当仅用在前面这样的短函数中,在长的函数中它们会影响代码的可读性。
函数,一种特殊的变量
前面提到,我们可以从变量的角度来看待和使用函数,那么,作为一种特殊的变量,函数自身也可以作为「参数」或者「返回值」在函数中出现:
用例子可以很好地说明:
// 传入了一个「返回值为 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 中
switch
的case
无需为常量,且取值不必为整数。
下面的示例程序可以很好地帮助我们理解这两条特性:
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 语言入门基础语法的主要内容,如果未能解决你的问题,请参考以下文章