Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题

101 阅读15分钟

Go 语言的一些基础特性

  1. 高性能、高并发:Go 语言内置了轻量级的协程(goroutines)和通道(channels),使得编写高并发程序变得非常简单,同时其性能表现也十分出色,可以媲美 C/C++ 等底层语言。
  2. 语法简单、学习曲线平缓:Go 语言的语法简洁明了,与 C 语言比较相似,如果有 C 语言基础的话,学习 Go 语言的基础语法会非常地轻松。
  3. 丰富的标准库:Go 拥有丰富的标准库,涵盖了网络编程、文件 I/O、数据处理等多个方面,开发者可以直接使用这些库,无需依赖第三方库,简化了开发流程。
  4. 完善的工具库:Go 提供了一套完善的工具链,包括代码格式化工具(gofmt)、代码分析工具、测试工具等,这些工具可以帮助开发者提高代码质量和开发效率。
  5. 静态链接:Go 支持静态链接,这意味着编译后的可执行文件包含了所有依赖的库,无需额外安装依赖,方便部署和分发。
  6. 快速编译:Go 的编译速度非常快,这得益于其简洁的语法和高效的编译器,可以缩短开发周期,提高开发效率。
  7. 跨平台:Go 支持跨平台编译,可以轻松地将代码编译成适用于不同操作系统(如Windows、Linux、macOS)和不同架构(如x86、ARM)的可执行文件。
  8. 垃圾回收:Go 内置了垃圾回收机制,自动管理内存分配和回收,减轻了开发者的负担,避免了内存泄漏等问题。

Go 语言下载与开发环境配置

第一步,Go 语言下载。进入官网 Go 语言 下载对应自己操作系统的安装包或压缩包,这里以 Windows 系统为例。

image.png 下载之后双击 go1.23.2.windows-amd64.msi 文件进行安装,安装完毕之后可以打开 Windows 的终端并输入以下命令查看是否安装成功:

> go version

若安装成功则会返回 Go 语言的版本信息字样,例如 go version go1.23.2 windows/amd64

第二步,开发环境下载。这里我选择简洁的 Visual Studio Code 作为我之后练习以及项目开发的开发环境。下载完之后,需要在扩展一栏安装 Go 插件,如下图所示: image.png 之后若要运行或编译自己写的 Go 语言代码,可以使用下述命令:

# 运行
> go run 路径名\文件名
# 编译
> go build 路径名\文件名

如果觉得每次操作有些麻烦,则可以在扩展那边下载一个名为 Code Runner 的插件,可以将上述命令行代码简化为一个按钮:

image.png

Go 语言基础语法

hello world

学习任何语言,都逃不过 hello world,以下是 Go 语言版本的 hello world。

package main

import (
	"fmt"
)

func main() {
	fmt.Println("hello world")
}

package main 声明了当前文件属于 main 包。main 包是一个特殊的包,它是程序的入口点。一个可执行程序必须包含 main 包。import () 是导入语句,用于引入其他包的功能,在这里,我们导入了 fmt 包,fmt 包是Go语言的标准库之一,提供格式化输入和输出的功能,例如打印文本到控制台或格式化字符串。func main() {} 定义了 main 函数。main 函数是程序的入口点,程序执行时会从 main 函数开始。

变量定义

Go 语言是一门强类型语言,这意味着每个变量都有一个特定的类型,并且在编译时进行类型检查。以下代码展示了 Go 语言中变量声明和使用的几种方式,以及常量的定义。

package main

import (
	"fmt"
	"math"
)

func main() {

	var a = "initial"

	var b, c int = 1, 2

	var d = true

	var e float64

	f := float32(e)

	g := a + "foo"
	fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0
	fmt.Println(g)                // initialapple

	const s string = "constant"
	const h = 500000000
	const i = 3e20 / h
	fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}

Go 语言有两种方法声明变量,一种是使用 var 进行声明,一种是使用:=进行声明。在使用 const 定义常量时,Go 会根据使用的上下文自动确定类型。

if-else

总体上来看,Go 语言的 if-else 与 C 语言相差不大,不过有两点需要注意:(1)C 语言中的 if 后面需要跟 (),而 Go 语言的 if 后面不需要跟 ()。(2)C 语言在写只有一行处理逻辑的 if 时,可以省略 {},而 Go 语言不能省略 {}。示例代码如下:

package main

import "fmt"

func main() {

	if 7%2 == 0 {
		fmt.Println("7 is even")
	} else {
		fmt.Println("7 is odd")
	}

	if 8%4 == 0 {
		fmt.Println("8 is divisible by 4")
	}

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

for

for 语句与 if-else 类似,即 for 后面不需要跟 (),且 {} 不能省略。示例代码如下:

package main

import "fmt"

func main() {

	i := 1
	for {
		fmt.Println("loop")
		break
	}
	for j := 7; j < 9; j++ {
		fmt.Println(j)
	}

	for n := 0; n < 5; n++ {
		if n%2 == 0 {
			continue
		}
		fmt.Println(n)
	}
	for i <= 3 {
		fmt.Println(i)
		i = i + 1
	}
}

switch

Go 语言的 switch 与 C 语言的 switch 有很多不同的地方:(1)C 语言的 switch 后面需要跟 (),而 Go 语言的 switch 不需要跟 ()。(2)Go语言的 switch 语句中,每个 case 都隐含了一个 break 语句,无需像 C 语言那样显式地添加 break。(3)Go 的 switch 语句可以用于比较各种数据类型,包括整数、浮点数、字符串、布尔值、指针、接口等。而 C 语言的 switch 语句只能用于比较整数类型。示例代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {

	a := 2
	switch a {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	case 4, 5:
		fmt.Println("four or five")
	default:
		fmt.Println("other")
	}

	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("It's before noon")
	default:
		fmt.Println("It's after noon")
	}
}

数组

对于数组来说,主要注意一下数组的定义(var a [5]intvar twoD [2][3]int)和初始化(b := [5]int{1, 2, 3, 4, 5}),数组的存取则与 C 语言一样。示例代码如下:

package main

import "fmt"

func main() {

	var a [5]int
	a[4] = 100
	fmt.Println("get:", a[2])
	fmt.Println("len:", len(a))

	b := [5]int{1, 2, 3, 4, 5}
	fmt.Println(b)

	var twoD [2][3]int
	for i := 0; i < 2; i++ {
		for j := 0; j < 3; j++ {
			twoD[i][j] = i + j
		}
	}
	fmt.Println("2d: ", twoD)
}

不过由于数组的大小在一开始就确定了,在实战中用处很少,一般情况都是使用切片来代替数组。

切片

Go 语言中的切片(slice)是一种动态数组,它提供了比数组更强大的功能和灵活性。切片是基于数组构建的,但长度可变,可以根据需要动态增长或缩减。在 Go 中,可以使用 make 来创建一个切片,切片可以用 append 来添加元素,看起来与 Python 的 append 很像,但是在 Python 中是不需要赋值回去的,而 Go 则需要将添加后的切片赋值回原来的切片(解释:当 append() 操作需要添加的元素超出切片的容量时,Go 会创建一个新的、更大的底层数组,并将原数组的元素复制到新数组中。然后,append() 返回一个指向新数组的新的切片。由于底层数组发生了变化,原始切片仍然指向旧的、较小的数组,因此需要将新的切片赋值回去)。此外,Go 的 slice 支持与 Python 类似的切片操作,原则为 "左闭右开"。示例代码如下:

package main

import "fmt"

func main() {

	s := make([]string, 3)
	s[0] = "a"
	s[1] = "b"
	s[2] = "c"
	fmt.Println("get:", s[2])   // c
	fmt.Println("len:", len(s)) // 3

	s = append(s, "d")
	s = append(s, "e", "f")
	fmt.Println(s) // [a b c d e f]

	c := make([]string, len(s))
	copy(c, s)
	fmt.Println(c) // [a b c d e f]

	fmt.Println(s[2:5]) // [c d e]
	fmt.Println(s[:5])  // [a b c d e]
	fmt.Println(s[2:])  // [c d e f]

	good := []string{"g", "o", "o", "d"}
	fmt.Println(good) // [g o o d]
}

map

Go 语言中的 map 是一种内置的数据结构,用于存储键值对(key-value pairs)。它是一种无序的集合,允许通过键快速查找、插入和删除对应的值。在 Go 中,可以使用 make 来创建一个 map,其中需要定义两个类型,第一个为 key 的类型,第二个为 value 的类型。Go 通过 r, ok := m["unknow"] 进行键的查询,若存在则 oktrue,反之为 false。此外,Go 使用 delete 进行键的删除。示例代码如下:

package main

import "fmt"

func main() {
	m := make(map[string]int)
	m["one"] = 1
	m["two"] = 2
	fmt.Println(m)           // map[one:1 two:2]
	fmt.Println(len(m))      // 2
	fmt.Println(m["one"])    // 1
	fmt.Println(m["unknow"]) // 0

	r, ok := m["unknow"]
	fmt.Println(r, ok) // 0 false

	delete(m, "one")

	m2 := map[string]int{"one": 1, "two": 2}
	var m3 = map[string]int{"one": 1, "two": 2}
	fmt.Println(m2, m3)
}

range

Go 语言中的 range 关键字用于迭代各种数据结构,例如数组、切片、字符串、map 和通道(channel)。它提供了一种简洁而方便的方式来访问集合中的元素。示例代码如下:

package main

import "fmt"

func main() {
	nums := []int{2, 3, 4}
	sum := 0
	for i, num := range nums {
		sum += num
		if num == 2 {
			fmt.Println("index:", i, "num:", num) // index: 0 num: 2
		}
	}
	fmt.Println(sum) // 9

	m := map[string]string{"a": "A", "b": "B"}
	for k, v := range m {
		fmt.Println(k, v) // b 8; a A
	}
	for k := range m {
		fmt.Println("key", k) // key a; key b
	}
}

函数

Go 语言与其他语言不同的是,变量类型是后置的(如 a int, b int 以及 func add(...) int {})。示例代码如下:

package main

import "fmt"

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

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

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

func main() {
	res := add(1, 2)
	fmt.Println(res) // 3

	v, ok := exists(map[string]string{"a": "A"}, "a")
	fmt.Println(v, ok) // A True
}

Go 原生支持返回多个值,其中 func exists 函数展示了一般业务逻辑的情况,即第一个返回真正的值(v),第二个返回错误信息(ok)。

指针

Go 语言支持指针,指针是一种存储变量内存地址的变量。指针允许我们直接访问和操作内存中的值,这在某些情况下可以提高性能并提供更灵活的内存管理。示例代码如下:

package main

import "fmt"

func add2(n int) {
	n += 2
}

func add2ptr(n *int) {
	*n += 2
}

func main() {
	n := 5
	add2(n)
	fmt.Println(n) // 5
	add2ptr(&n)
	fmt.Println(n) // 7
}

可以观察到,在执行 add2 函数之后,n 的值并没有发生变化,因为传入函数的 n 只是一个拷贝后的值,对其进行操作并不会改变原来的 n。而使用指针类型则可以实现对值的修改,但需要注意类型匹配,在传入的时候需要在值的前面添加一个 & 符号,同时在取值进行操作的时候需要在值的前面加一个 * 符号,这一点与 C 语言一样。

结构体

在 Go 语言中,结构体(struct)是一种复合数据类型,它允许我们将不同类型的数据组合在一起,形成一个新的数据类型。示例代码如下:

package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	a := user{name: "wang", password: "1024"}
	b := user{"wang", "1024"}
	c := user{name: "wang"}
	c.password = "1024"
	var d user
	d.name = "wang"
	d.password = "1024"

	fmt.Println(a, b, c, d)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
	fmt.Println(checkPassword(a, "haha"))   // false
	fmt.Println(checkPassword2(&a, "haha")) // false
}

func checkPassword(u user, password string) bool {
	return u.password == password
}

func checkPassword2(u *user, password string) bool {
	return u.password == password
}

结构体方法

在 Go 语言中,结构体方法是一种与结构体类型关联的函数。通过定义方法,开发者可以在结构体上实现特定的行为,使得结构体更像对象,从而支持面向对象编程的特性,类似于其他语言中的类成员函数。示例代码如下:

package main

import "fmt"

type user struct {
	name     string
	password string
}

func (u user) checkPassword(password string) bool {
	return u.password == password
}

func (u *user) resetPassword(password string) {
	u.password = password
}

func main() {
	a := user{name: "wang", password: "1024"}
	a.resetPassword("2048")
	fmt.Println(a.checkPassword("2048")) // true
}

注意与普通函数的区别,以 checkPassword 为例,u user 的位置在 checkPassword 后的为普通方法,u user 的位置在 func 后且在 checkPassword 前的为结构体方法。

错误处理

Go 语言的错误处理机制与其他语言(例如 Java 或 Python)的异常处理机制有所不同。Go 语言不使用异常处理,而是通过返回错误值的方式来处理错误。这种方式使得错误处理更加显式,便于开发者在程序中对错误进行适当的处理。示例代码如下:

package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil
		}
	}
	return nil, errors.New("not found")
}

func main() {
	u, err := findUser([]user{{"wang", "1024"}}, "wang")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(u.name) // wang

	if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
		fmt.Println(err) // not found
		return
	} else {
		fmt.Println(u.name)
	}
}

字符串操作

Go 语言的字符串操作简洁而高效,主要围绕内置的 string 类型和 strings 标准库包展开。示例代码如下:

package main

import (
	"fmt"
	"strings"
)

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

字符串格式化

fmt 包是 Go 语言标准库中一个非常重要的包,主要用于格式化输入输出。它类似于 C 语言中的 printf 和 scanf,但功能更强大,也更安全。示例代码如下:

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", n)  // 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
}

与 C 语言不同的是,Go 语言中可以直接使用 %v 来打印任意类型的变量,而不需要去区分具体的变量类型。此外,Go 还可以通过 %+v 来获得详细的结果以及 %#v 来获得更加详细的结果。最后,Go 语言在打印保留 2 位小数的格式化操作与 C 语言一样,都是 %.2f

JSON 处理

Go 语言内置了强大的 JSON 处理能力,主要通过 encoding/json 标准库包实现。这个包提供了一系列函数和类型,可以轻松地将 Go 数据结构编码成 JSON 格式,以及将 JSON 数据解码成 Go 数据结构。示例代码如下:

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"}}
}

首先要注意的是,结构体中的变量名必须要大写开头,如果想在 JSON 中显示为小写,则需要使用反引号(`)来定义标签,如上述代码中的 Age 变量(`json:"age"` 表示在 JSON 中该字段的名称是 age,而在 Go 代码中该字段的名称是 Age)。

时间处理

在 Go 语言中,时间处理主要通过 time 包来实现。这个包提供了丰富的功能来处理时间和日期,包括获取当前时间、时间格式化、时间计算、时间解析等。示例代码如下:

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
}

数字解析

Go 语言的 strconv 包提供了丰富的函数,用于将字符串转换为数字类型以及将数字类型转换为字符串。示例代码如下:

package main

import (
	"fmt"
	"strconv"
)

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

	n, _ := strconv.ParseInt("111", 10, 64)
	fmt.Println(n) // 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
}

strconv.ParseFloat 可以将字符串转化为浮点数,其中第一个参数为字符串,第二个参数为精度(如 32 位、64 位)。strconv.ParseInt 可以将字符串转化为整数,其中第一个参数为字符串,第二个参数为进制,第三个参数为精度。strconv.Atoi 可以快速把一个十进制字符串转化为数字,如果传入的不是十进制字符串,则会在第二个返回值 err 中显示具体错误信息。

进程信息

在 Go 语言中,获取和管理进程信息通常涉及使用 os 和 os/exec 包。这些包提供了与操作系统交互的功能,包括获取当前进程信息、执行外部命令、管理进程等。

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"))

	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(buf)) // 127.0.0.1       localhost
}

我们可以使用 os.Args 来获取进程在执行时的一些命令行参数。此外,我们可以使用 os.Getenvos.Setenv 来获取或写入环境变量。

总结

通过此次 Go 语言基础语法的学习,我了解到了 Go 语言与其他语言的一些相同之处和不同之处,并且学到了 Go 语言中比较经典的一些标准库的应用,感受到了 Go 语言的优秀之处。最后,我觉得学习一门新语言需要善于挖掘它与其他语言的共性与差异,这样学起来才能事半功倍。