Go 语言的一些基础特性
- 高性能、高并发:Go 语言内置了轻量级的协程(goroutines)和通道(channels),使得编写高并发程序变得非常简单,同时其性能表现也十分出色,可以媲美 C/C++ 等底层语言。
- 语法简单、学习曲线平缓:Go 语言的语法简洁明了,与 C 语言比较相似,如果有 C 语言基础的话,学习 Go 语言的基础语法会非常地轻松。
- 丰富的标准库:Go 拥有丰富的标准库,涵盖了网络编程、文件 I/O、数据处理等多个方面,开发者可以直接使用这些库,无需依赖第三方库,简化了开发流程。
- 完善的工具库:Go 提供了一套完善的工具链,包括代码格式化工具(gofmt)、代码分析工具、测试工具等,这些工具可以帮助开发者提高代码质量和开发效率。
- 静态链接:Go 支持静态链接,这意味着编译后的可执行文件包含了所有依赖的库,无需额外安装依赖,方便部署和分发。
- 快速编译:Go 的编译速度非常快,这得益于其简洁的语法和高效的编译器,可以缩短开发周期,提高开发效率。
- 跨平台:Go 支持跨平台编译,可以轻松地将代码编译成适用于不同操作系统(如Windows、Linux、macOS)和不同架构(如x86、ARM)的可执行文件。
- 垃圾回收:Go 内置了垃圾回收机制,自动管理内存分配和回收,减轻了开发者的负担,避免了内存泄漏等问题。
Go 语言下载与开发环境配置
第一步,Go 语言下载。进入官网 Go 语言 下载对应自己操作系统的安装包或压缩包,这里以 Windows 系统为例。
下载之后双击 go1.23.2.windows-amd64.msi 文件进行安装,安装完毕之后可以打开 Windows 的终端并输入以下命令查看是否安装成功:
> go version
若安装成功则会返回 Go 语言的版本信息字样,例如 go version go1.23.2 windows/amd64。
第二步,开发环境下载。这里我选择简洁的 Visual Studio Code 作为我之后练习以及项目开发的开发环境。下载完之后,需要在扩展一栏安装 Go 插件,如下图所示:
之后若要运行或编译自己写的 Go 语言代码,可以使用下述命令:
# 运行
> go run 路径名\文件名
# 编译
> go build 路径名\文件名
如果觉得每次操作有些麻烦,则可以在扩展那边下载一个名为 Code Runner 的插件,可以将上述命令行代码简化为一个按钮:
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]int、var 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"] 进行键的查询,若存在则 ok 为 true,反之为 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.Getenv 或 os.Setenv 来获取或写入环境变量。
总结
通过此次 Go 语言基础语法的学习,我了解到了 Go 语言与其他语言的一些相同之处和不同之处,并且学到了 Go 语言中比较经典的一些标准库的应用,感受到了 Go 语言的优秀之处。最后,我觉得学习一门新语言需要善于挖掘它与其他语言的共性与差异,这样学起来才能事半功倍。