Back to Blogs
golang
backend
programming

Golang 入门到精通

Soloman
2022-08-14

Golang 入门到精通

1 环境与命令

在编译目标程序时,可以设置GOOS和GOARCH环境变量,来配置程序的目标运行平台,实现跨平台编译。

1.1 GOOS:设定程序目标运行平台

  • Mac:GOOS=darwin
  • Linux:GOOS=linux
  • Windows:GOOS=windows

1.2 GOARCH:设定程序目标运行平台的处理器架构

  • 386:GOARCH=386
  • AMD64:GOARCH=amd64
  • ARM:GOARCH=arm

1.3 Go 常用命令

// 查看当前Go环境变量
go env

// 直接运行源代码程序
go run xx.go
go run *.go

// 编译程序,-o 编译结果文件名称
go build -o xx.exe xx.go
go build *.go

// 安装编译好的程序到GOBIN目录下(GOPATH下的bin目录,需要在环境变量中设置GOBIN)
go install

2 数组与切片

Golang 普通数组为固定长度,而切片就是非定长的数组,其长度和容量会随着元素增多而增大。

package main

import "fmt"

func main() {
	cities := [8]string {"重庆", "成都", "北京", "上海", "广州", "深圳", "杭州", "南京"}

	city1 := cities[2:6]
	fmt.Println("city1: ", city1, "cities: ", cities)
	// 切片引用原数组,可看作原数组部分元素的别名,修改切片会导致修改原数组的元素
	city1[2] = "三亚"
	fmt.Println("city1: ", city1, "cities: ", cities)

	// copy()函数直接复制一份数据,修改新值不会影响原数组
	copyCities := make([]string, len(cities))
	copy(copyCities, cities[:])
	fmt.Println("copySites: ", copyCities, "cities: ", cities)
	copyCities[0] = "山城雾都"
	fmt.Println("copySites: ", copyCities, "cities: ", cities)

	cities[4] = "广州again"
	city2 := cities[:2]
	city3 := cities[6:]
	city4 := cities[2:5]
	fmt.Println("city2: ", city2, "city3: ", city3, "city4: ", city4)

	for index, value := range cities {
		fmt.Println(index+1, ".", value)
	}

	// 第二个参数为切片长度,第三个参数为切片容量
	sites := make([]string, 2, 2)
	fmt.Println("sites: ", sites)
	sites[0] = "www.soloman.vip"
	sites[1] = "www.sundaydesign.top"
	fmt.Println("sites: ", sites, "len: ", len(sites), "cap: ", cap(sites))
	sites = append(sites, "www.golang.org")
	fmt.Println("sites: ", sites, "len: ", len(sites), "cap: ", cap(sites))
	sites = append(sites, "www.python.org")
	sites = append(sites, "www.google.com")
	// 增加元素,会动态改变切片长度和容量大小。len()获取切片长度,cap()获取切片容量
	fmt.Println("sites: ", sites, "len: ", len(sites), "cap: ", cap(sites))

	for _, site := range sites {
		fmt.Println(site)
	}
}

3 字典

使用make()函数分配空间时,建议指定具体长度,这样性能会更好。集合不存在访问越界,对于不存在的键会返回值对应数据类型的零值(布尔:false、数字:0、字符串:空字符串)

package main

import "fmt"

func main() {
	siteMap := make(map[string]string, 5)
	siteMap["name"] = "Soloman"
	siteMap["domain"] = "soloman.vip"
	siteMap["category"] = "personal site"
	siteMap["developer"] = "Solomanxbr"
	siteMap["technology"] = "Python/Golang/MySQL/Redis/Nginx/Linux"
	printMap(siteMap)

	siteMap["host"] = "www.soloman.vip"
	delete(siteMap, "technology")
	delete(siteMap, "category")
	printMap(siteMap)

	// 通过ok为true/false来判断集合中是否存在该键
	value, ok := siteMap["ip_address"]
	if ok {
		fmt.Println("ip address:", value)
	} else {
		fmt.Println("Doesn't has ip address info:", ok)
	}
}

// 遍历打印集合键值对
func printMap(mapData map[string]string) {
	fmt.Println("------------------")
	for key, value := range mapData {
		fmt.Println(key, ": ", value)
	}
	fmt.Println("------------------")
}

4 函数

函数名首字母大写,为 public,可在其他包中导入使用;首字母小写的函数为 private, 只能在包内使用。内存逃逸:Golang 编译器自动管理内存,在编译时会判断并将需要的变量从栈移动到堆上。

package main

import "fmt"

func main() {
	score, greeting, flags := funcExample(10, "Soloman", false)
	fmt.Printf("%s! your score is %d. The final exam passed:%t", greeting, score, flags)
}

func funcExample(num int, name string, ok bool) (ret int, msg string, flg bool) {
	ret = num * 10
	msg = "Nice to meet you, " + name
	flg = !ok
	return
}

5 switch case

os.Args 可获取命令行运行程序时的输入参数,如:命令行输入./switch_case.exe site hello则os.Args获取到切片["/Code/switch_case.exe", "site", "hello"],第一个元素为程序所在路径,其后则为传入的参数

package main

import (
	"fmt"
	"os"
)

func main() {
	cmds := os.Args
	if len(cmds)<2 {
		fmt.Println("Please input more parameters...", cmds)
	} else {
		for index, cmd := range cmds {
			fmt.Printf("%d %s\n", index, cmd)
		}
		switch cmds[1] {
		case "hi":
			fmt.Println("Hello")
		case "site":
			fmt.Println("www.soloman.vip")
		case "bye":
			fmt.Println("Bye Bye")
		default:
			fmt.Println("I don't know")
		}
	}
}

6 init 和 defer

在导入一个包时,会自动执行该包中的所有 init() 函数。

  • init() 函数没有参数,也没有返回值
  • 一个包可以包含多个 init() 函数,调用顺序不确定
func init() {
	fmt.Println("Do some init operations when this package been imported.")
}

defer 用于定义延迟操作的关键字,确保程序退出当前栈时会执行相关操作。

  • 一般用于定义清理资源的操作,如打开文件后关闭文件、替代锁的作用等
  • 在同一函数中有多个 defer 时,执行顺序类似栈:先进后出
func deferExample() {
	f1, err := os.Open("temp.log")
    // 定义并调用匿名函数
	defer func() {
		fmt.Println("Ready to close file.")
		_ = f1.Close()
	}()

	if err != nil {
		fmt.Println("An error occurred when open file:", err)
	} else {
		buf := make([]byte, 256)
		cnt, _ := f1.Read(buf)
		fmt.Printf("Read content: %d\n%s", cnt, string(buf))
	}

	defer fmt.Println("I am executed before file closed.")
	defer fmt.Println("I am executed on the second.")
	defer fmt.Println("\nI am executed on the first after file content.")
}

/*
Read content: 172
contents in temp.log
I am executed on the first after file content.
I am executed on the second.
I am executed before file closed.
Ready to close file.
*/

7 类的封装、继承与多态

7.1 封装

Golang 没有 class 关键字,通过结构体来定义类属性并绑定类方法。封装一个 Site 类,以指针和值副本的方式分别绑定 Introduce1() 和 Introduce2() 方法。

package main

import "fmt"

func main() {
	site1 := Site{
		"Soloman",
		"www.soloman.vip",
		"Python/Golang/MySQL/Redis",
		3,
		8888.88,
	}
	site2 := site1

	fmt.Println("使用指针绑定方法")
	fmt.Println("修改之前:", site1)
	site1.Introduce1()
	fmt.Println("修改之后:", site1)

	fmt.Println("使用值副本绑定方法")
	fmt.Println("修改之前:", site2)
	site2.Introduce2()
	fmt.Println("修改之前:", site2)
}

// Site 定义类属性
type Site struct {
	name string
	host string
	technology string
	age int
	price float64
}

// Introduce1 指针形式绑定方法,会修改site1本身
func (this *Site) Introduce1() {
	fmt.Printf("My name is %s, my host is %s, I'm %d years old, built by %s, worth %f dollars.\n",
		this.name, this.host, this.age, this.technology, this.price)
	this.host = "www.sunnie.fun"
	this.age = 6
	this.price = 9999.66
	fmt.Printf("My name is %s, my host is %s, I'm %d years old, built by %s, worth %f dollars.\n",
		this.name, this.host, this.age, this.technology, this.price)
}

// Introduce2 值副本形式绑定方法,不会修改site2本身
func (this Site) Introduce2() {
	fmt.Printf("My name is %s, my host is %s, I'm %d years old, built by %s, worth %f dollars.\n",
		this.name, this.host, this.age, this.technology, this.price)
	this.host = "www.sunnie.fun"
	this.age = 8
	this.price = 9999.88
	fmt.Printf("My name is %s, my host is %s, I'm %d years old, built by %s, worth %f dollars.\n",
		this.name, this.host, this.age, this.technology, this.price)
}

7.2 继承

注意区分结构体嵌套与类继承的区别。Golang 中的 public 和 private 都是通过首字母大小写来区分,在 import 导包和类中都是这样。在不同包中,想导入使用其他包中的类属性和类方法,则相关属性和方法首字母必须大写。

package main

import "fmt"

func main() {
	soloman := Student{
		person: Person{
			"Soloman",
			17,
			"男",
			false,
		},
		score: 98,
		class: "高二(1)班",
		school: "清华附中",
	}

	teacherMa := Teacher{
		grade: "教授",
		subject: "量子力学",
		school: "清华大学",
	}
	teacherMa.name = "马老师"
	teacherMa.age = 50
	teacherMa.gender = "男"
	teacherMa.adult = true

	fmt.Println(soloman)
	fmt.Println(teacherMa)
	soloman.Saying()
	teacherMa.Speaking()
}

// Person 定义父类
type Person struct {
	name string
	age int
	gender string
	adult bool
}

// Student 嵌套 Person
type Student struct {
	person Person
	score int
	class string
	school string
}

// Teacher 继承 Person
type Teacher struct {
	Person
	grade string
	subject string
	school string
}

func (stu *Student) Saying() {
	fmt.Printf("My name is %s, I'm a student come from %s, %d years old.\n",
		stu.person.name, stu.school, stu.person.age)
}

func (tch *Teacher) Speaking() {
	fmt.Printf("My name is %s, I'm a %s come from %s, good at %s",
		tch.name, tch.grade, tch.school, tch.subject)
}

7.3 多态

接口可以接受任何数据类型,一般用于根据不同数据类型做不同的程序处理。接口基本使用:

package main

import "fmt"

func main() {
	arr := make([]interface{}, 5)
	arr[0] = "www.soloman.vip"
	arr[1] = 999
	arr[2] = 3.1415926
	arr[3] = false
	
	for _, value := range arr {
		switch v := value.(type) {
		case int:
			fmt.Printf("Value %d is a number.\n", v)
		case float64:
			fmt.Printf("Value %f is a float,\n", v)
		case string:
			fmt.Printf("Value %s is a string.\n", v)
		case bool:
			fmt.Printf("Value %v is an bool.\n", v)
		default:
			fmt.Println("Not a legal value:", v)
		}
	}
}

接口实现多态:

package main

import "fmt"

func main() {
	//var creature Creature
	soloman := Human{
		name: "Soloman",
		gender: "boy",
		age: 17,
	}
	goldFish := Fish{
		name: "Fairy of Water",
		category: "gold fish",
	}
	eagle := Bird{
		name: "King of the Sky",
		category: "bird",
	}

	//creature = &soloman
	//creature.Move()
	//creature.Speak()
	Introduce(&soloman)
	Introduce(&goldFish)
	Introduce(&eagle)
}

// Creature 接口及其方法
type Creature interface {
	Move()
	Speak()
}

type Human struct {
	name string
	gender string
	age int
}

type Fish struct {
	name string
	category string
}

type Bird struct {
	name string
	category string
}

// 绑定 Human 类到接口
func (man *Human) Move() {
	fmt.Printf("My name is %s, %d years old %s, I can walk run and swimming.\n",
		man.name, man.age, man.gender)
}

func (man *Human) Speak() {
	fmt.Printf("My name is %s, %d years old %s, I can speak Chinese and English.\n",
		man.name, man.age, man.gender)
}

// 绑定 Fish 类到接口
func (fish *Fish) Move() {
	fmt.Printf("My name is %s, I'm a %s, I can swimming fast under water.\n",
		fish.name, fish.category)
}

func (fish *Fish) Speak() {
	fmt.Printf("My name is %s, I'm a %s, I can speak Fishingish: gu gu gu\n",
		fish.name, fish.category)
}

// 绑定 Bird 类到接口
func (bird *Bird) Move() {
	fmt.Printf("My name is %s, I'm a %s, I can fly in the sky.\n",
		bird.name, bird.category)
}

func (bird *Bird) Speak() {
	fmt.Printf("My name is %s, I'm a %s, I can speak Birdish: ji jo ji jo\n",
		bird.name, bird.category)
}

// 通用接口
func Introduce(living Creature) {
	living.Move()
	living.Speak()
}

8 并发

三种退出方式对比:

  • return 退出当前函数
  • Exit 退出当前进程
  • Goexit 退出当前 routine
package main

import (
	"os"
	"runtime"
)

func main() {
	go func() {
		runtime.Goexit()
	}()
	os.Exit(-1)
	return
}

多 routine 间通信使用管道:

  1. 当缓冲区写满时,写阻塞,当被读取后,恢复写入
  2. 当读取完数据后,读阻塞,当再次有数据写入管道时,恢复读取
  3. 如果管道没有使用 make 分配空间,那默认为 nil,读写都会阻塞,即无法进行读写
  4. 对于一个管道,读写必须次数对等,否则若阻塞在主程序,则程序会崩溃;若阻塞在子 routine,那么会出现内存泄漏
package main

import (
	"fmt"
	"time"
)

func main() {
	intChan := make(chan int, 5)
	go func() {
		for i:=0; i<10; i++ {
			fmt.Println("routine 1 写入数据:", i)
			intChan <- i * 5
		}
	}()

	go func() {
		for i:=10; i<20; i++ {
			fmt.Println("routine 2 写入数据:", i)
			intChan <- i * 5
		}
	}()

	for i:=0; i<20; i++ {
		data := <-intChan
		fmt.Println("main 读取数据:", data)
		time.Sleep(50 * time.Millisecond)
	}
	fmt.Println("main 结束!")
}

为了使对一个管道的读写次数对等,一般使用 for range 遍历管道数据:

  • 使用 close 重复关闭同一个管道程序会崩溃
  • 向已关闭的管道继续写入数据,程序会崩溃
  • 从已关闭的管道读取数据,会返回零值,程序不会崩溃
  • 关闭管道的操作应该在写入数据端,因为读取数据端不知道何时(即写入端还会写入数据吗)关闭管道
package main

import (
	"fmt"
)

func main() {
	intChan := make(chan int, 5)
	go func() {
		for i:=0; i<20; i++ {
			fmt.Println("routine 写入数据:", i)
			intChan <- i
		}
		close(intChan)
	}()

	// for range 遍历未关闭的管道不知道何时停止,会一直等待读取新写入的数据
	// 在写入端 使用 close 关闭管道,for range 遍历完关闭的管道后即退出
	for num := range intChan {
		fmt.Println("main 读取数据:", num)
	}
	fmt.Println("main 结束!")
}

通过ok-idom模式,即num, ok := <-intChan判断管道的关闭状态:

package main

import (
	"fmt"
)

func main() {
	intChan := make(chan int, 5)
	go func() {
		for i:=0; i<20; i++ {
			fmt.Println("routine 写入数据:", i)
			intChan <- i
		}
		close(intChan)
	}()

	for {
		num, ok := <-intChan
		if !ok {
			fmt.Println("已取完数据,管道已关闭,main 准备退出...")
			break
		}
		fmt.Println("读取数据:", num)
	}
	fmt.Println("main 退出")
}

8.1 单向管道

之前一直定义的都是双向通道,既可以写入,又可以读取。Golang 还支持定义单向通道:

  • 双向通道:intChan := make(chan int, 5)
  • 单向读通道:intChanReadOnly := make(<-chan int, 5)
  • 单向写通道:intChanWriteOnly := make(chan<- int, 5)

单向管道一般用于函数参数,这样语义更明确:

package main

import (
	"fmt"
	"time"
)

func main(){
	intChan := make(chan int, 5)
	// 参数为单向只写通道
	go producer(intChan)
	// 参数为单向只读通道
	go consumer(intChan)
	
	time.Sleep(time.Second)
	fmt.Println("main over!")
}

// 生产者向管道写入数据
func producer(in chan<- int) {
	for i:=0; i<20;i++ {
		fmt.Println("生产者写入:", i)
		in <- i
	}
}

// 消费者从管道取数据
func consumer(out <-chan int) {
	for num := range out {
		fmt.Println("消费者读取:", num)
	}
}

8.2 监听通道

当一个程序中有多个通道协作时,需要对每个通道进行监听,每当某个通道有数据时程序需要作出一些操作,可使用 select case 语句。

package main

import (
	"fmt"
	"time"
)

func main() {
	intChan1 := make(chan int, 5)
	intChan2 := make(chan int, 5)

	// 使用 select case 监视两个 channel
	go func() {
		for {
			fmt.Println("监听通道...")
			select {
			case num1 := <-intChan1:
				fmt.Println("读取到 intChan1 的数据:", num1)
			case num2 := <-intChan2:
				fmt.Println("读取到 intChan2 的数据:", num2)
			default:
				fmt.Println("两个管道都未读取到数据...:")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	// 向 intChan1 写数据
	go func() {
		for i:=0; i<10;i++ {
			fmt.Println("intChan1 写入:", i)
			intChan1 <- i
			time.Sleep(500 * time.Millisecond)
		}
	}()

	// 向 intChan2 写数据
	go func() {
		for i:=10; i<20;i++ {
			fmt.Println("intChan2 写入:", i)
			intChan2 <- i
			time.Sleep(time.Second)
		}
	}()

	for {
		fmt.Println("main over!")
		time.Sleep(5 * time.Second)
	}
}