Go 快速上手 | 青训营笔记

251 阅读11分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 1 篇笔记。

特点

  • 高性能、高并发
  • 语法简单、学习曲线平缓
  • 丰富的标准库
  • 完善的工具链
  • 静态编译
  • 快速编译
  • 跨平台
  • 垃圾回收

开发环境

本地

  1. 安装 Go

  2. 配置集成环境

    • VS Code + Go 插件
    • GoLand

基础语法

Go 语言每一行末尾不需要添加分号 ; ,但添加也不会报错。

Hello World

package main // 指定程序的包

// 导入外部的包
import (
	"fmt"
)

// main 函数
func main() {
	fmt.Println("Hello World")
}

在 Go 中,只有 main 包下的程序才可以执行。

使用 go run main.go 运行程序,使用 go build main.go 把程序编译成二进制文件,再使用 ./main 执行程序。

变量

Go 是强类型的语言,变量需要指定变量类型。常见的类型包括字符串、整数、浮点型、布尔型。

字符串是内置类型,可以通过加号 + 拼接,也可以用等号 == 比较两个字符串。

运算符的优先级与 C/C++ 类似。

变量的声明

变量的声明有两种方式。

一种是通过 var 关键字声明。var variable[, variable1, ...] [type] [= value [, value2, ...]]

var a = "initial"
var b,c int = 1, 2
var d = true
var e float64

通过这种方式声明,一般会自动推导类型,但也可以显式的指定 var b, c int = 1, 2

另一种是通过 := 的方式命名。variable := value

f := float32(e)

常量的声明

与变量的 var 声明类似,只是把 var 改为 constconst variable [type] = value

注意,常量没有确定的类型,是根据使用的上下文自动确定的。

const s string = "constant"
const h = 5
const i = 3e20 / h

条件控制

if else

Go 的 if 语句与 C/C++ 类似,但有一些区别。

  • if 的条件不需要添加括号,即使加了括号,在编译时也会去掉
  • if 的条件后面要跟大括号,不能换行,也不能省略,即使只有一条语句
  • if 语句不能写成一行,即左大括号输入后就需要换行,即使只有一条语句
if 8%4 == 0 {
    fmt.Println("8 is divisible by 4")
}
if 7%2 == 0 {
    fmt.Println("7 is even")
} else {
    fmt.Println("7 is odd")
}
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")
}

switch

Go 的 switch 与 C/C++ 类似,但也有区别。

  • switch 后面的变量名不需要括号
  • 每个 case 默认不需要添加 break
  • 变量可以使用任意的变量类型
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")
}

switch 可以取代 if 语句,只需要在 switch 后不写变量名即可。

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

循环

Go 只有 for 循环,没有 whiledo while

i := 1
for i <= 3 {
    fmt.Println(i)
    i = i + 1
}

for 循环如果不写条件,就是死循环。

for {
    fmt.Println("loop")
    break
}

for 循环可以使用类似 C/C++ 的三段式结构,这三段任何一段都可以省略。

for j := 7; j < 9; j++ {
    fmt.Println(j)
}

for 循环支持使用 continue 跳过循环,使用 break 跳出循环。

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

数组

数组是长度固定的,元素类型一样的数据结构类型,其中每个元素都具有编号。

Go 可以使用 var 来定义数组。var name [size]type

var a [5]int

可以在定义数组的时候初始化数组。

b := [5]int{1, 2, 3, 4, 5}

eg:定义一个二维数组并遍历

var twoD [2][3]int
for i := 0; i < 2; i++ {
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}

切片

切片与数组类似,但是切片是可变长的。

  • 使用 make 创建空的切片 value := make([]type, len)

    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
    
  • 使用 append 追加元素,需要注意的是,append 后需要把结果赋值给原变量。

    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println(s) // [a b c d e f]
    
  • 使用 copy(a, b) 在两个切片拷贝数据

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println(c) // [a b c d e f]
    
  • 切片支持像 Python 那样的切片操作

    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

map 是一种 Key-Value 的数据类型。map 中的元素是无序的。

  • 使用 make 创建空 map value := make(map[key]value)

    m := make(map[string]int)
    
  • 访问 map 的元素

    访问 map 中的 key 时,与访问数组的元素类似,中括号 [ ] 中写 Key。

    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
    

    访问 map 中的元素时,实际是返回两个值,一个是 key 的值(key 不存在返回 0);另一个是布尔值,如果 key 存在,返回 true,否则返回 false。

    r, ok := m["unknow"]
    fmt.Println(r, ok) // 0 false
    
  • 使用 delete 删除 map 中的元素 delete(map, key)

    delete(m, "one")
    
  • 创建有初值的 map

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

range

可以使用 range 来遍历切片或 map

对于切片,range 会返回两个值,第一个是切片的下标,另一个是对于下标的值。

nums := []int{2, 3, 4}
sum := 0
//如果不需要下标,可以把 i 换成 _
for i, num := range nums { 
    sum += num
    if num == 2 {
        fmt.Println("index:", i, "num:", num) // index: 0 num: 2
    }
}
fmt.Println(sum) // 9

对于 map,range 会返回两个值,第一个是 key, 第二个是 value。

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 中,函数的返回类型是后置的,支持返回多个值。这在实际应用中,一般第一个是返回函数的值,第二个是返回错误信息。

函数名首字母大写表示这个函数是可导出的,即在其他程序中,只需要导入包后,即可通过 package.func 来调用这个函数。

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 中,一般用于对传入的值进行修改, * 用来声明一个指针。

在使用指针时, * 用来取消指针的引用, & 用来获取变量的地址。

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
}

结构体

结构体是带类型字段的集合,是自定义数据类型。使用 type 定义结构体。

type user struct {
	name     string
	password string
}

使用结构体的名称初始化一个变量。

//使用名字指定初始化的值,允许只初始化一部分,没有初始化的赋初始值
a := user{name: "wang", password: "1024"}
c := user{name: "wang"}
//如果没有使用名字指定,需要按照变量定义的类型和顺序传参
b := user{"wang", "1024"}

也可以使用 var 初始化一个空的结构体对象。

var d user

使用 variable.field 访问结构体的内容。

d.name = "wang"
d.password = "1024"

结构体类型也可以当作函数的参数,同时支持指针。

package main

import "fmt"

type user struct {
	name     string
	password string
}

func main() {
	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
}

结构体方法

这与 Java 等其他面向对象语言的对象方法类似。

注意,结构体方法并不是在定义结构体时写在里面,而是在 func 关键字后添加 (name struct)

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
}

错误处理

Go 可以使用 if-else 去处理错误。

package main

import (
	"errors"
	"fmt"
)

type user struct {
	name     string
	password string
}

// 在函数返回类型中加一个 error 类型的参数,表示这个函数会返回错误
func findUser(users []user, name string) (v *user, err error) {
	for _, u := range users {
		if u.name == name {
			return &u, nil //如果正常,就返回值和 nil
		}
	}
	return nil, errors.New("not found") // 如果不正常,就返回 nil 和错误
}

func main() {
    // 如果不需要处理错误,可以把 err 换成 _
	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)
	}
}

字符串操作

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 包中,有很多处理字符串格式化的函数。

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}

    //不需要像 C 那样区分类型,直接用 %v,可以使用 %+v 显示详细的结果, %#v 更加详细
	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
}

JSON 处理

在 Go 中,结构体中变量首字母大写为可导出的变量,类似于 Java 中的 public 变量。

package main

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name  string
	Age   int `json:"age"` // json 的 tag,Age 在序列化时会转成 tag 的值
	Hobby []string
}

func main() {
	a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
    //使用 Marshal 函数需要保证结构体的每个变量都是共有的,即大写字母开头
	buf, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(buf)         // [123 34 78 97...]
    //输出 json 对象时需要使用 string(),否则会输出一些十六进制的值
    //这也推出 json 对象实质是一个字符数组
    //默认序列化的结果中的 key 是大写的,可以在结构体的字段后面加个 json 的 tag
	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
    //使用 Unmarshal 进行反序列化,把一个 json 字符串转成 对象
	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"}}
}

时间处理

处理时间相关的函数在 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
    //time.Date(year, month, day, hour, minute, second, millisecond, zone) 构造一个时间对象
	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
    //输出格式化的时间,2006-01-02 15:04:05 是固定的
	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
    //把字符串解析成时间,2006-01-02 15:04:05 是固定的
	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
}

数字解析

有关数字解析的函数都在 strconv 中。

package main

import (
	"fmt"
	"strconv"
)

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

    //(str, str进制, 精度)
	n, _ := strconv.ParseInt("111", 10, 64)
	fmt.Println(n) // 111

    // 0 表示自动推断 str 的进制类型
	n, _ = strconv.ParseInt("0x1000", 0, 64)
	fmt.Println(n) // 4096

    //把一个十进制字符串转为数字
	n2, _ := strconv.Atoi("123")
	fmt.Println(n2) // 123

    //把数字转为字符串
	n3 := strconv.Itoa(123)
	fmt.Println(n3) // "123"
    
	n2, err := strconv.Atoi("AAA")
	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
}

进程信息

通过 os 包获取进程信息。

package main

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

func main() {
	// os.Args 获取进程执行时的命令行参数
	// 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
}

Go 学习线路图

image-20220509110923612