Go语言入门指南Part 1 | 青训营

128 阅读10分钟

Go语言

Go语言也称为 Golang,是由 Google 公司开发的一种静态强类型、编译型、并发型、并具有垃圾回收功能的编程语言。

Go语言拥有高性能高并发语法简洁垃圾回收静态链接标准库工具链跨平台快速编译等特点,下面让我们来一起学习Go语言的入门基础语法吧。

1、从Hello World说起

package main

import "fmt"

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

package main 表示包名为 main 的包为应用程序的入口包,若源码中没有 main 包,将无法编译出可执行的文件。 且 main 包中必须包含 main 函数。在这段代码中,我们导入了外部的 fmt 包,并使用其中的 Println 函数向屏幕输出了"Hello,World"。

2、变量

(1) 变量的声明和初始化

声明变量的一般形式是使用var关键字:

var name type

其中name表示变量的名字,type表示变量的类型。 或者我们可以使用简短格式去初始化变量,系统会自动帮我们推导变量的类型。需要注意的是,这样初始化要求 不提供数据类型只能用在函数内部

package main

func main() {

	var a int //显式声明
	a = 1
	b := 2 //自动推导
	println(a, b)
}

我们可以在同一行内声明多个变量,也可以在同一行内赋值:

package main

func main() {

	a, b := 1, 2
	println("a=", a, "b=", b)
	a, b = b, a
	println("a=", a, "b=", b)
}

输出结果为:

a= 1 b= 2

a= 2 b= 1

可以看到,我们在一行内完成了 ab 的初始化,并利用这个性质实现了两个数字间的交换。

如果我们只是想声明变量,每次都打 var 太累了,能不能更轻松呢? 显然是可以的,Go语言为我们提供了批量声明的方法:

var (
	a int
	b string
	c []float64
)

Go语言中定义常量需要使用const关键字:

const s = "Hello,World!"
fmt.Println(s)

要特别注意常量只能用上面这种方法初始化,var:=都会报错。

(2) 数据类型

Go语言有以下数据类型:

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte

rune

float32 float64

complex64 complex128

floatint后面跟着的数字表示数据的比特数,Go语言默认的整型和浮点型是int64(64位操作系统下)和float64

Go语言不能像C语言一样进行自动类型转换,这意味着如果你要将一个浮点型的变量赋值给一个整型,必须得像下面这样写:

  a := 3.5
	var b int
	b = int(a)
	fmt.Printf("a的类型是%T,b的类型是%T", a, b)

这里我们使用了%T占位符来获取变量的数据类型,输出结果为a的类型是float64,b的类型是int

3、流程控制

(1) 分支结构

Go的条件分支语句如if-elseswitch和流程控制语句breakcontinuegoto等与C语言类似,但是略有区别:

if语句的主要区别在于描述条件时不需要圆括号()

我们可以在条件前面添加一个执行语句,其作用范围被限制在if-else语句范围中。

if (可执行语句) condition {

//...

} else {

//...

}

有两个特别需要注意的点:

左花括号必须和if-else及条件在同一行

else ifelse必须和上一个分支的右花括号在同一行

例子如下:

func main() {
	a := 1
	if a > 1 { //不需要括号
		fmt.Println("Big!")
	} else {
		fmt.Println("Small!")
	}
	if b := 2 * a; b > 1 { //可以放一个可执行语句
		fmt.Println("Big!")
	} else {
		fmt.Println("Small!")
	}
}

输出结果为Small!Big!

switch语句的主要区别在于Go的switch不需要在每个分支都加上break,只要符合某个 case 就不会执行到下一 case。当然如果非要和C语言中的switch语句兼容可以使用fallthrough关键字,这样执行完一个 case 后仍然会继续下一个 case。同时一个 case 可能有多种取值,如下所示:

func main() {
	var s = "hello"
	switch s {
	case "not hello":
		fmt.Println("I am not hello.")
	case "hello", "happy": //一分支多值的case
		fmt.Println("I am hello.")
	default:
		fmt.Println("I don't know.")
	}
}

输出为I am hello.

Go语言中switch拥有更为强大的功能,它可以像if-else语句一样添加表达式:

func main() {
	a := 1
	switch {
	case a > 1:
		fmt.Println("Big!")
	default:
		fmt.Println("Small!")
	}
}

(2) 循环结构

Go语言中只有for循环而没有while循环,其用法和C语言中基本一致,唯一区别是没有了圆括号()。 同时,当我们想要写一个死循环的时候也不再需要写成类似for(;;)这样的,只需要不写条件表达式即可。 我们也可以写出只有循环条件的循环,这个时候不需要打两个多余的;,功能相当于while

package main

import "fmt"

func main() {
	//普通for循环
	for i := 1; i <= 5; i++ {
		fmt.Printf("%v ", i)
	}
	//只保留条件的for循环(相当于while)
	j := 1
	for j <= 3 {
		fmt.Printf("%v ", j)
		j++
	}
	//死循环
	for {
		fmt.Println("Loop!")
		break
	}
}

输出结果为1 2 3 4 5 1 2 3 Loop!

键值循环

for range结构是Go语言特有的一种的迭代结构,可以遍历数组、切片、字符串、mapchannel等。其类似于其他语言中的for each,如C++的for (auto a : b)和python的for a in b。在Go语言中,for range结构一般按如下格式:

for key,val := range coll {

do something...

}

在字符串和数组中,key表示的是遍历元素的下标,val表示的是值;在map中,分别表示的是索引键和其对应的值。需要注意的是,for range是只读的,因此在循环中改变val的值也不会改变实际数组。

package main

import "fmt"

func main() {

	a := make([]int, 0)
	a = append(a, 1)
	a = append(a, 2)
	a = append(a, 3)
	//遍历数组
	for k, v := range a {
		fmt.Println(k, v)
		v = 114514 //对拷贝做修改是影响不到原数组的,可以看下面遍历a数组时值仍然没发生变化
	}
	//如果不需要知道下标,将k设置为匿名变量即可
	for _, v := range a {
		fmt.Println(v)
	}
	mp := map[string]int{
		"aa": 1,
		"bb": 2,
	}
	//遍历map
	for k, v := range mp {
		fmt.Println(k, v)
	}
}


break: Go语言中的break支持指定跳出的循环,而非单纯的跳出最近一层的循环(默认情况)。只需要在break后加上需要跳出的循环:

package main

func main() {

label1:
	for i := 1; i <= 20; i++ {
	label2:
		for j := 1; j <= 20; j++ {
			if i+j >= 20 {
				break label2
			} else if i >= 10 {
				break label1
			}
		}
	}
}

4、容器

(1)、数组

Go语言中数组是长度固定的,其声明语法为:

var name [size] type

默认情况下数组的元素会赋为零值(对int来说是0)。

我们也可以使用字面值,用一组值去初始化数组:

var a [3]int = [3]int{1, 2, 3}

当然我们可以用:=来做字面值初始化,同时可以用...来代替右侧指定的数组长度。简化后代码如下:

a := [...]int{1, 2, 3}

我们可以通过下标访问数组,遍历数组时,还可以通过前面讲过的键值循环。

package main

func main() {

	var b [5]string //声明数组
	b[1] = "abc"    //下标索引
	println(len(b)) //输出数组长度

	a := [...]int{1, 2, 3} //字面值初始化数组
	for _, v := range a {  //键值循环访问数组元素
		println(v)
	}
}

数组的长度是数组类型的组成部分之一,这就是说[3]int[2]int不是同一种类型,不能用!===判断两个数组是否相等,也不能用=去赋值。数组在Go语言中属于值类型,所以将一个数组赋值给另一个数组是拷贝了值而非地址。

(2)、切片

Go语言中切片是可以动态增长的对数组中连续片段引用的引用类型。其内部结构包含DataLenCap。其中Data是指向存储空间的指针。

直接声明切片

只需要在声明数组的语句中不声明长度即可声明切片。

使用make函数构造切片

a := make([]type, size , capacity)

前两个参数分别表示切片的类型初始分配的大小,第三个参数表示预分配的元素数量,可省略,其设定后不影响size,只在于提前分配空间避免多次分配空间带来的性能问题。

从数组或切片中生成新的切片

格式与python的切片类似:

slice [begin : end]

切片截取的区间是左闭右开的。注意切片是引用类型,所以切片和截取出来的新切片中Data指向同一块内存。因此这种方式生成的切片修改/添加时,会修改原本数组/切片的值。

package main

import "fmt"

func main() {
	//直接声明新的切片
	var a []int
	d := []int{1, 2, 3, 4, 5}
	//用make函数声明切片
	b := make([]string, 3)
	b[0] = "apple"
	b[1] = "banana"
	b[2] = "cat"
	for _, v := range b {
		fmt.Printf("%v ", v)
	}
	fmt.Println()
	//从数组/切片中生成新的切片
	c := b[1:2]
	fmt.Println(len(a), len(b), len(c), len(d))
	c[0] = "dog" //c是从b中生成的切片,对c做修改会对b本身也修改.
	for _, v := range b {
		fmt.Printf("%v ", v)
	}
}

向切片中添加元素

可以用append函数为切片添加元素。需要注意的是要写成a = append(a,..)的形式,即将append返回的切片赋值回原本的切片。

package main

import "fmt"

func main() {
	var a []int
	a = append(a, 1)
	a = append(a, 2, 3, 4)
	a = append(a, []int{5, 6, 7}...)
	for _, v := range a {
		fmt.Println(v)
	}
}

输出结果为1 2 3 4 5 6 7

我们也可以换一下函数参数的位置实现从切片的开头添加元素:

package main

import "fmt"

func main() {
	var a []int
	a = append(a, 1)
	a = append([]int{2, 3, 4}, a...)
	for _, v := range a {
		fmt.Println(v)
	}
}

输出结果为2 3 4 1

复制切片

复制切片使用copy函数即可,copy(a,b)会将切片b复制给切片a

  a := []int{1, 2, 3}
	b := []int{4, 5, 6, 7, 8}
	copy(b, a)
	fmt.Println(b)

输出结果为[1 2 3 7 8],即被复制切片长度不够时只会把长度范围内的数据复制到目标切片去。

  a := []int{1, 2, 3}
	b := []int{4, 5, 6, 7, 8}
	copy(a, b)
	fmt.Println(a)

输出结果为[4 5 6],即被复制切片比目标切片长的时候,只会将被复制切片对应长度的数据复制过去。

删除元素

虽然没有删除元素的函数,但是我们可以利用append实现。

  a := []int{1, 2, 3, 4, 5}
	fmt.Println(a)
	pos := 2
	//删除第pos个元素
	a = append(a[:pos], a[pos+1:]...)
	fmt.Println(a)

输出结果为[1 2 3 4 5][1 2 4 5]

(3)、map

map是其他语言中的字典/映射/哈希表,当然也是引用类型

声明/初始化方式与切片是非常类似的,只需要注意格式是map[type1]type2,表示从type1type2的映射。

package main

import "fmt"

func main() {
	var mp1 map[string]int                //声明一个从string -> int的映射
	mp2 := map[string]int{"a": 1, "b": 1} //直接声明+初始化
	mp3 := make(map[string]int)           //使用make函数声明,这和 := map[string]int{}是等价的
	mp3["c"] = 3
	fmt.Println(len(mp1), len(mp2), len(mp3))
	fmt.Println(mp2)
	fmt.Println(mp3)
}

map的遍历和查找

遍历map可以使用前面提到的键值循环。

for k, v := range mp2 {
		fmt.Println(k, v)
	}

查找某个索引是否在map中存在时,可以用多值接收返回值,接收的第二个参数表示是否存在,如果存在是true,否则是false

  val, ok := mp2["a"]
	fmt.Println(val, ok)
	val, ok = mp2["c"]
	fmt.Println(val, ok)

输出结果为1 true0 false

map的删除

delete(map名,键值)可以从map中删除指定的键值。

package main

import "fmt"

func main() {
	mp := map[string]int{"a": 1, "b": 2, "c": 3}
	fmt.Println(mp)
	delete(mp, "b")
	fmt.Println(mp)
}

输出结果为map[a:1 b:2 c:3]map[a:1 c:3]