Go语言基础语法 | 青训营笔记

101 阅读12分钟

这是我参与「第五届青训营 」笔记创作活动的第1天。

简介

Go语言(又称Golang)是由Google开发的一种开源编程语言,于2009年推出。它是一种编译型语言,具有静态类型以及类似C语言的语法和性能。Go语言的主要特点有:简洁易懂的语法、高效的内存管理(有垃圾回收器)、优异的高并发性能、丰富的标准库和完善的工具链、对网络编程和大型系统开发有良好的支持,平缓的学习曲线使其广泛应用于后端开发、微服务、分布式系统以及物联网等领域。

入门

1. 开发环境

1.1 安装Golang

安装步骤很简单。

  1. 官网下载对应系统的安装包。如果网络环境恶劣的话,也可以尝试从Golang中国的镜像下载。
  2. 运行安装包并按提示安装。
  3. 打开命令行工具,运行go version命令检查Go是否安装成功。

1.2 配置集成开发环境

配置Go的集成开发环境可以使开发更加方便高效,推荐选用VSCode或Goland。

以VSCode为例,安装好VSCode之后只需要在插件市场中搜索安装Go插件,然后就可以开始愉快地写代码啦。

1.3 基于云的环境

大家还可以用gitpod的云环境来编写Go,只需要打开网站用github登录即可,十分的方便Nice!

2. 基础语法

2.1 第一个程序

学一门新语言,写的第一个程序当然是从输出Hello, world开始啦。

package main

import (
    "fmt"
)

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

我们的第一个程序大概长这个样子。第一行的package main表明这个文件属于main包,main包也就是我们程序的入口包。第三行的import语句导入了Go的fmt包,这个包主要用来向屏幕输入输出以及格式化字符串。最后我们定义了main函数,用fmt包的Println方法输出了Hello, world!的字符串。

写完之后保存为hello.go就可以尝试运行啦,Go中构建代码的命令为go build <filename>,也可以用go run <filename>构建并运行。

$ go run hello.go
Hello, world!
$ go build hello.go
$ ./hello
Hello, world!

2.2 变量与常量

Go是一门强类型语言,每一个变量都有对应的类型。常见类型有整数浮点数字符串布尔型等。

Go语言中定义变量有两种方式:

  • 使用var关键字定义变量,如var a int = 16
  • 使用缩写的:=语法。如b := 3.1415926

var关键字时需要指定变量类型或初始值,编译器可以根据初始值自动推导相应变量类型,也可以由指定变量类型默认初始化为对应类型的零值;用:=定义变量需要注意,变量必须是没有被定义过的,否则会报错:

var a = "This is a string"
var b float // 初始化为0
var c bool = false
c := true //error: no new variables on left side of :=

还可以同时定义多个变量,如果变量值不重要可以用_来接受并忽略:

var name, country string = "John Doe", "US"
_, country := "John Doe", "US"

Go中字符串属于内置类型,可以使用+拼接,也可以使用==判断。运算符的使用和优先级参考c/c++。

常量需要使用关键字const,编译器可以自动由上下文推导常量的类型:

const s string = "const string"
const pi = 3.1415926

2.3 条件语句

Go中的ifelse与c/c++类似,不同点是判断条件不需要用圆括号,里面允许声明一个变量(很方便),此外ifelse后边必须跟着左花括号(c++选手表示不适):

if num := 9; num < 0 {
    fmt.Println(num, "is negative.")
} else if num < 10 {
    fmt.Println(num, "has one digit.")
} else {
    fmt.Println(num, "has multiple digits.")
}

Go中的switch语句也与c/c++类似,不过同样的switch后边判断的变量不需要用圆括号。此外,每一个case语句不需要用break跳出,Go中执行完对应的case语句后会默认跳出switch结构。如果多个case对应同一段代码逻辑,可以用case var1, var2:的方式合并起来。

var grade string = "B"  
var marks int = 90  
  
switch marks {  
    case 90: grade = "A"  
    case 80: grade = "B"  
    case 50,60,70 : grade = "C"  
    default: grade = "D"    
}

2.4 循环语句

Go中只有一种for循环,有三种形式。

  • 和C语言的for一样:
// 与C语言一样,循环条件语句的三段可以任意省略
for j := 7; j < 9; j++ {
    fmt.Println(j)
}
  • 和C语言的while一样:
i:=1
for i < 5 {
    fmt.Println(i)
    i=i+1
}
  • 和C语言的for(;;;)一样:
for {
    fmt.Println("for loop")
    break
}

类似的,Go语言也有breakcontinue语句,用法与C语言一致。

2.5 数组

Go中的数组是一种固定长度的元素序列,用来存储同一类型的数据。数组的长度是固定的,在定义时需要指定,如:

var balance1 [10]float32
var balance2 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance[0] = 1000.0
var balances = [3][3]float32{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
fmt.Println(balance1, len(balance1))

需要注意的是,在Go语言中数组的长度是数组类型的一部分,因此[5]int[25]int是不同的类型。由于数组的长度固定,在真实业务场景下很少用数组,更多的是用接下来介绍的切片。

2.6 切片

Go语言中的切片是对数组的一种抽象,是可变长度的数组,定义方式如下:

// 使用var关键字定义切片
var numbers []int
// 使用make函数来创建切片
b := make([]int, 3, 5)

当使用make函数定义切片时,需要指定切片的类型、长度,可以选择性指定切片容量。那什么是切片的长度和容量呢?切片实现的原理是存储了一个等于容量大小的数组、当前使用的长度以及指向数组的指针。往切片中添加元素时长度会相应增加,如果超出容量,就会扩容并返回一个新的切片。

切片追加元素需要使用append函数:

b = append(b, 5)

注意append函数是内建函数,不同于其他面向对象编程语言中的成员方法,因此只能用赋值的方式来追加(不适+1)。

最后Go中的切片拥有和python中一样的切片操作,但是不支持负索引:

fmt.Println(b[0:2])
fmt.Println(b[1:])
fmt.Println(b[:2])

2.7 map

在其他语言里,它可能叫做哈希表或者字典,是实际业务中使用最频繁的数据结构。

定义方式如下:

// 可以通过var关键字定义
var ages map[string]int
// 可以通过make函数创建
countries := make(map[string]int)
// 也可以通过字面值的形式创建
genders := map[string]string{"Alice": "Female", "Bob": "Male"}

查找值的方式与C++一致,如genders["Alice"],删除某个键值对用delete函数,如delete(genders, "Alice")

2.8 range

对于数组、切片、字符串及map等,我们可以用range来快速便利,从而使代码优雅简洁,如:

nums := []int{1,2,3,4}
for i, n := range nums {
    fmt.Println(i, n)
}
ages := map[string]int{"Alice": 31, "Bob", 25}
for name, age := range ages {
    fmt.Println(name, age)
}

对于数组和切片,range会返回索引和对应的值,对于map会返回键值对,如果不需要索引可以用_接受并忽略。

2.9 函数

Go中的函数定义语法如下:

func function_name(arg1 type1, arg2, type2,...) return_type {
    // 函数体
}

Go中变量类型都是写到后面的,return_type是返回类型,如果函数没有返回值可以省略返回类型。Go中原生支持返回多个值:

var exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}

在实际的业务代码中,几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个是错误信息。

2.10 指针

Go中的指针不同于C/C++,支持的操作有限,主要用来在函数中修改参数的值。

package main

import "fmt"

func modify(p *int) {
    *p = 100
}

func main() {
    x := 1
    p := &x
    fmt.Println(*p)
    modify(p)
    fmt.Println(*p)
}

2.11 结构体

结构体由一系列带类型的字段构成。定义示例如下:

type Person struct {
    Name string
    Age int
}

初始化结构体变量时需要传入每个字段的值:

p := Person{"John Doe", 30}

或者用键值对的方式,从而只对一部分字段进行初始化:

p := Person{Name: "John Doe", Age: 30}
q := Person{Name: "Bob"}

结构体也支持指针,通常将结构体变量指针作为函数参数,以减小大结构体变量的拷贝开销:

func information(p *Person) {
    fmt.Println(p.Name, p.Age)
}

information(&p)

结构体方法是在结构体类型上定义的函数,与普通函数的区别在于结构体方法的func关键字多了接收器,就像函数的参数一样使其可以访问结构体的字段,如果接收器类型写为指针的话还可以修改字段的值,如:

type Person struct {
    Name string
    Age int
}

func (p Person) SayHello() {
    fmt.Println("Hello, my name is", p.Name)
}

func (p *Person) ChangeName(name String) {
    p.name = name
}

func main() {
    p := Person{"John Doe", 30}
    p.SayHello()
    p.ChangeName("Bob")
    p.SayHello()
}

2.12 错误处理

Go中习惯的做法是使用一个单独的返回值来传递错误信息。如果没有错误,就返回原本的结果和nil;如果出现了错误,就返回nil和对应的error

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    fmt.Println(err)
}
fmt.Println(result)

2.13 字符串操作

在标准库strings包里有很多常用的字符串工具函数:

package main

import (
    "fmt"
    "strings"
)

func main() {
    s:="hello"
    fmt.Println(strings.Contain(s, "ll")) //true
    fmt.Println(strings.Count(s, "l") //2
    fmt.Println(strings.HasPrefix(s, "hel")) //true
    fmt.Println(strings.HasSuffix(s, "llo")) //true
    fmt.Println(strings.Index(s, "ll") //2
    fmt.Println(strings.Join([]string{"he", "llo"}, "-")) //he-llo
    fmt.Println(strings.Repeat(s, 2)) //hellohello
    fmt.Println(strings.Replace(s, "e", "E", -1)) //hEllo
    fmt.Println(strings.Split("a-b-c", "-")) //[a b c]
    fmt.Println(strings.ToLower(s)) //hello
    fmt.Println(strings.ToUpper(s)) //HELLO
    fmt.Println(len(s)) //5
}

2.14 字符串格式化

fmt包中有很多字符串格式化相关的方法,比如Printf函数类似于C语言中的printf,不同的是Go中可以用%v来打印任意类型的变量,使用%+v%#v可以打印更加详细的结果。

package main

import "fmt"

type point struct {
    x, y int
}

func main() {
    s := "hello"
    n := 123
    p := point{1, 2}
    fmt.Println(s, n) // hello 123
    fmt.Println(p) // {1 2}
    fmt.Printf("s=%v\n", s) //s=hello
    fmt.Printf("n=%v\n", s) //n=123
    fmt.Printf("p=%v\n", p) //p={1 2}
    fmt.Printf("p=%+v\n", p) //p={x:1 y:2}
    fmt.Printf("p=%#v\n", p) //p=main.Point{x:1, y:2}
    
    f := 3.141592653
    fmt.Println(f) //3.141592653
    fmt.Printf("%.2f\n", f) //3.14
}

2.15 JSON处理

Go语言中内置了encoding/json包,提供了处理JSON数据的函数。使用json.Marshal函数可以将Go数据结构体编码为JSON,使用json.Unmarshal函数可以将JSON数据解码为Go数据结构体。其中结构体的各个字段名首字母应该大写,即保证各字段为公开字段。我们也可以在字段定义后面用标签tag语法来指定该字段在编码为JSON时的名字。标签是一个字符串,可以在反引号中定义,如果没有标签则使用字段名。

package main

import (
    "encoding/json"
    "fmt"
)

type userInfo struct {
    Name  string
    Age   int `json:"age"`
    Hobby []string
}

func main() {
    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
    buf, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }
    fmt.Println(buf)  // [123 34 78 97...]
    fmt.Println(String(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
    
    buf, err = json.MarshalIndent(a, "", "\t")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(buf))
    
    var b userInfo
    err = json.Unmarshal(buf, &b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", b) //main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}

2.16 时间处理

Go语言标准库中提供了很多用于处理时间的函数和类型。常用的有time包,它提供了对时间的抽象表示,包括时间点、时间段和时区。time.Time类型表示一个时间点,可以通过调用time.Now()获取当前时间,也可以调用time.Data()来获取带时区的时间。可以使用format字符串对时间点进行格式化,也可以使用Parse函数将字符串解析为时间点。此外,还提供了时间差的计算、时区的处理和间隔的表示等功能。在与不同系统交互时,可以用.Unix()来获取时间戳。

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
	t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
	t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
	fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
	diff := t2.Sub(t)
	fmt.Println(diff)                           // 1h5m0s
	fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
	t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
	if err != nil {
		panic(err)
	}
	fmt.Println(t3 == t)    // true
	fmt.Println(now.Unix()) // 1648738080
}

2.17 数字解析

Go中字符串与数字之间的转换可以用strconv这个包来完成。我们可以用ParseInt将字符串解析为整数,第二个参数为进制数,设为0时由字符串前缀自动推导,第三个参数为bit位大小。同样的,ParseFloat可以解析为浮点数,第二个参数也为bit位大小。我们也可以用Atoi将十进制字符串转为数字,或者用Itoa把数字转为字符串。

package main

import (
	"fmt"
	"strconv"
)

func main() {
	f, _ := strconv.ParseFloat("1.234", 64)
	fmt.Println(f) // 1.234

	n, er := strconv.ParseInt("11111111", 10, 8)
	fmt.Println(n, er) // 111

	n, _ = strconv.ParseInt("0x1000", 0, 64)
	fmt.Println(n) // 4096

	n2, _ := strconv.Atoi("123")
	fmt.Println(n2) // 123

	n2, err := strconv.Atoi("AAA")
	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

2.18 进程信息

Go的os包提供了与操作系统交互的一些函数。比如我们可以用os.Args来获取程序执行时的命令行参数,用os.Getenvos.Setenv来获取和设定环境变量,也可以用os/exec包中的Command函数执行命令。

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	// go run example/20-env/main.go a b c d
	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
	fmt.Println(os.Setenv("AA", "BB"))
	fmt.Println(os.Getenv("AA"))

	buf, err := exec.Command("grep", "-E", `[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`, "/etc/hosts").CombinedOutput()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf)) // 127.0.0.1       localhost
}

进阶

好啦,现在我们对Go已经有了一个整体的认识,后面我们就可以做一些小的项目啦~(咕咕咕)