Go 语言简明教程

您所在的位置:网站首页 go语音入门 Go 语言简明教程

Go 语言简明教程

2023-08-25 02:38| 来源: 网络整理| 查看: 265

Go 语言简明教程

Go 简明教程系列文章链接:

Go 语言简明教程 (Aug 6, 2019) Go Gin 简明教程 (Aug 7, 2019) Go2 新特性简明教程 (Aug 15, 2019) Go Protobuf 简明教程 (Jan 11, 2020) Go RPC & TLS 鉴权简明教程 (Jan 13, 2020) Go WebAssembly (Wasm) 简明教程 (Jan 23, 2020) Go Test 单元测试简明教程 (Feb 10, 2020) Go Mock (gomock)简明教程 (Feb 14, 2020) Go Mmap 文件内存映射简明教程 (Apr 20, 2020) Go Context 并发编程简明教程 (Apr 20, 2020)

Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。 —— Go - wikipedia.org

1 Go 安装

最新版本下载地址官方下载 golang.org,当前是 1.13.6。如无法访问,可以在 studygolang.com/dl 下载

使用 Linux,可以用如下方式快速安装。

123456$ wget https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz$ tar -zxvf go1.13.6.linux-amd64.tar.gz$ sudo mv go /usr/local/$ go versiongo version go1.13.6 linux/amd64

从 Go 1.11 版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题。

1$ go env -w GOPROXY=https://goproxy.cn,direct

或在 ~/.profile 中设置环境变量

1export GOPROXY=https://goproxy.cn 2 Hello World

新建一个文件 main.go,写入

1234567package mainimport "fmt"func main() { fmt.Println("Hello World!")}

执行go run main.go 或 go run .,将会输出

12$ go run .Hello World!

如果强制启用了 Go Modules 机制,即环境变量中设置了 GO111MODULE=on,则需要先初始化模块 go mod init hello否则会报错误:go: cannot find main module; see ‘go help modules’

我们的第一个 Go 程序就完成了,接下来我们逐行来解读这个程序:

package main:声明了 main.go 所在的包,Go 语言中使用包来组织代码。一般一个文件夹即一个包,包内可以暴露类型或方法供其他包使用。 import “fmt”:fmt 是 Go 语言的一个标准库/包,用来处理标准输入输出。 func main:main 函数是整个程序的入口,main 函数所在的包名也必须为 main。 fmt.Println(“Hello World!”):调用 fmt 包的 Println 方法,打印出 “Hello World!”

go run main.go,其实是 2 步:

go build main.go:编译成二进制可执行程序 ./main:执行该程序 3 变量与内置数据类型3.1 变量(Variable)

Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1,在 Go 语言中,需要这么写:

123var a int // 如果没有赋值,默认为0var a int = 1 // 声明时赋值var a = 1 // 声明时赋值

var a = 1,因为 1 是 int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写,这种方式还有一种更简单的表达:

12a := 1msg := "Hello World!" 3.2 简单类型

空值:nil

整型类型: int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …

浮点数类型:float32, float64

字节类型:byte (等价于uint8)

字符串类型:string

布尔值类型:boolean,(true 或 false)

12345var a int8 = 10var c1 byte = 'a'var b float32 = 12.2var msg = "Hello World"ok := false 3.3 字符串

在 Go 语言中,字符串使用 UTF8 编码,UTF8 的好处在于,如果基本是英文,每个字符占 1 byte,和 ASCII 编码是一样的,非常节省空间,如果是中文,一般占3字节。包含中文的字符串的处理方式与纯 ASCII 码构成的字符串有点区别。

我们看下面的例子:

1234567891011121314package mainimport ( "fmt" "reflect")func main() { str1 := "Golang" str2 := "Go语言" fmt.Println(reflect.TypeOf(str2[2]).Kind()) // uint8 fmt.Println(str1[2], string(str1[2])) // 108 l fmt.Printf("%d %c\n", str2[2], str2[2]) // 232 è fmt.Println("len(str2):", len(str2)) // len(str2): 8} reflect.TypeOf().Kind() 可以知道某个变量的类型,我们可以看到,字符串是以 byte 数组形式保存的,类型是 uint8,占1个 byte,打印时需要用 string 进行类型转换,否则打印的是编码值。 因为字符串是以 byte 数组的形式存储的,所以,str2[2] 的值并不等于语。str2 的长度 len(str2) 也不是 4,而是 8( Go 占 2 byte,语言占 6 byte)。

正确的处理方式是将 string 转为 rune 数组

12345str2 := "Go语言"runeArr := []rune(str2)fmt.Println(reflect.TypeOf(runeArr[2]).Kind()) // int32fmt.Println(runeArr[2], string(runeArr[2])) // 35821 语fmt.Println("len(runeArr):", len(runeArr)) // len(runeArr): 4

转换成 []rune 类型后,字符串中的每个字符,无论占多少个字节都用 int32 来表示,因而可以正确处理中文。

3.4 数组(array)与切片(slice)

声明数组

12var arr [5]int // 一维var arr2 [5][5]int // 二维

声明时初始化

12var arr = [5]int{1, 2, 3, 4, 5}// 或 arr := [5]int{1, 2, 3, 4, 5}

使用 [] 索引/修改数组

12345arr := [5]int{1, 2, 3, 4, 5}for i := 0; i < len(arr); i++ { arr[i] += 100}fmt.Println(arr) // [101 102 103 104 105]

数组的长度不能改变,如果想拼接2个数组,或是获取子数组,需要使用切片。切片是数组的抽象。 切片使用数组作为底层结构。切片包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展

声明切片:

1234slice1 := make([]float32, 0) // 长度为0的切片slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片fmt.Println(len(slice2), cap(slice2)) // 3 5

使用切片:

123456789// 添加元素,切片容量可以根据需要自动扩展slice2 = append(slice2, 1, 2, 3, 4) // [0, 0, 0, 1, 2, 3, 4]fmt.Println(len(slice2), cap(slice2)) // 7 12// 子切片 [start, end)sub1 := slice2[3:] // [1 2 3 4]sub2 := slice2[:3] // [0 0 0]sub3 := slice2[1:4] // [0 0 1]// 合并切片combined := append(sub1, sub2...) // [1, 2, 3, 4, 0, 0, 0] 声明切片时可以为切片设置容量大小,为切片预分配空间。在实际使用的过程中,如果容量不够,切片容量会自动扩展。 sub2... 是切片解构的写法,将切片解构为 N 个独立的元素。 3.5 字典(键值对,map)

map 类似于 java 的 HashMap,Python的字典(dict),是一种存储键值对(Key-Value)的数据解构。使用方式和其他语言几乎没有区别。

123456789// 仅声明m1 := make(map[string]int)// 声明时初始化m2 := map[string]string{ "Sam": "Male", "Alice": "Female",}// 赋值/修改m1["Tom"] = 18 3.6 指针(pointer)

指针即某个值的地址,类型定义时使用符号*,对一个已经存在的变量,使用 & 获取该变量的地址。

1234str := "Golang"var p *string = &str // p 是指向 str 的指针*p = "Hello"fmt.Println(str) // Hello 修改了 p,str 的值也发生了改变

一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go 语言中,参数是按值传递的,如果不使用指针,函数内部将会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果参数使用指针,对参数的传递将会影响到外部变量。

例如:

12345678910111213141516func add(num int) { num += 1}func realAdd(num *int) { *num += 1}func main() { num := 100 add(num) fmt.Println(num) // 100,num 没有变化 realAdd(&num) fmt.Println(num) // 101,指针传递,num 被修改} 4 流程控制(if, for, switch)4.1 条件语句 if else12345678910111213age := 18if age < 18 { fmt.Printf("Kid")} else { fmt.Printf("Adult")}// 可以简写为:if age := 18; age < 18 { fmt.Printf("Kid")} else { fmt.Printf("Adult")} 4.2 switch1234567891011121314151617type Gender int8const ( MALE Gender = 1 FEMALE Gender = 2)gender := MALEswitch gender {case FEMALE: fmt.Println("female")case MALE: fmt.Println("male")default: fmt.Println("unknown")}// male 在这里,使用了type 关键字定义了一个新的类型 Gender。 使用 const 定义了 MALE 和 FEMALE 2 个常量,Go 语言中没有枚举(enum)的概念,一般可以用常量的方式来模拟枚举。 和其他语言不同的地方在于,Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough,例如: 12345678910111213switch gender {case FEMALE: fmt.Println("female") fallthroughcase MALE: fmt.Println("male") fallthroughdefault: fmt.Println("unknown")}// 输出结果// male// unknown 4.3 for 循环

一个简单的累加的例子,break 和 continue 的用法与其他语言没有区别。

1234567sum := 0for i := 0; i < 10; i++ { if sum > 50 { break } sum += i}

对数组(arr)、切片(slice)、字典(map) 使用 for range 遍历:

123456789101112131415161718nums := []int{10, 20, 30, 40}for i, num := range nums { fmt.Println(i, num)}// 0 10// 1 20// 2 30// 3 40m2 := map[string]string{ "Sam": "Male", "Alice": "Female",}for key, value := range m2 { fmt.Println(key, value)}// Sam Male// Alice Female 5 函数(functions)5.1 参数与返回值

一个典型的函数定义如下,使用关键字 func,参数可以有多个,返回值也支持有多个。特别地,package main 中的func main() 约定为可执行程序的入口。

123func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) { // body}

例如,实现2个数的加法(一个返回值)和除法(多个返回值):

12345678910111213func add(num1 int, num2 int) int { return num1 + num2}func div(num1 int, num2 int) (int, int) { return num1 / num2, num1 % num2}func main() { quo, rem := div(100, 17) fmt.Println(quo, rem) // 5 15 fmt.Println(add(100, 17)) // 117}

也可以给返回值命名,简化 return,例如 add 函数可以改写为

1234func add(num1 int, num2 int) (ans int) { ans = num1 + num2 return} 5.2 错误处理(error handling)

如果函数实现过程中,如果出现不能处理的错误,可以返回给调用者处理。比如我们调用标准库函数os.Open读取文件,os.Open 有2个返回值,第一个是 *File,第二个是 error, 如果调用成功,error 的值是 nil,如果调用失败,例如文件不存在,我们可以通过 error 知道具体的错误信息。

12345678910111213import ( "fmt" "os")func main() { _, err := os.Open("filename.txt") if err != nil { fmt.Println(err) }}// open filename.txt: no such file or directory

可以通过 errorw.New 返回自定义的错误

12345678910111213141516171819import ( "errors" "fmt")func hello(name string) error { if len(name) == 0 { return errors.New("error: name is null") } fmt.Println("Hello,", name) return nil}func main() { if err := hello(""); err != nil { fmt.Println(err) }}// error: name is null

error 往往是能预知的错误,但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。

123456789func get(index int) int { arr := [3]int{2, 3, 4} return arr[index]}func main() { fmt.Println(get(5)) fmt.Println("finished")} 1234$ go run .panic: runtime error: index out of range [5] with length 3goroutine 1 [running]:exit status 2

在 Python、Java 等语言中有 try...catch 机制,在 try 中捕获各种类型的异常,在 catch 中定义异常处理的行为。Go 语言也提供了类似的机制 defer 和 recover。

123456789101112131415func get(index int) (ret int) { defer func() { if r := recover(); r != nil { fmt.Println("Some error happened!", r) ret = -1 } }() arr := [3]int{2, 3, 4} return arr[index]}func main() { fmt.Println(get(5)) fmt.Println("finished")} 1234$ go run .Some error happened! runtime error: index out of range [5] with length 3-1finished 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。 6 结构体,方法和接口6.1 结构体(struct) 和方法(methods)

结构体类似于其他语言中的 class,可以在结构体中定义多个字段,为结构体实现方法,实例化等。接下来我们定义一个结构体 Student,并为 Student 添加 name,age 字段,并实现 hello() 方法。

12345678910111213141516type Student struct { name string age int}func (stu *Student) hello(person string) string { return fmt.Sprintf("hello %s, I am %s", person, stu.name)}func main() { stu := &Student{ name: "Tom", } msg := stu.hello("Jack") fmt.Println(msg) // hello Jack, I am Tom} 使用 Student{field: value, ...}的形式创建 Student 的实例,字段不需要每个都赋值,没有显性赋值的变量将被赋予默认值,例如 age 将被赋予默认值 0。 实现方法与实现函数的区别在于,func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。 调用方法通过 实例名.方法名(参数) 的方式。

除此之外,还可以使用 new 实例化:

1234func main() { stu2 := new(Student) fmt.Println(stu2.hello("Alice")) // hello Alice, I am , name 被赋予默认值""} 6.2 接口(interfaces)

一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。

举一个简单的例子,定义一个接口 Person和对应的方法 getName() 和 getAge():

123456789101112131415161718192021222324252627282930type Person interface { getName() string}type Student struct { name string age int}func (stu *Student) getName() string { return stu.name}type Worker struct { name string gender string}func (w *Worker) getName() string { return w.name}func main() { var p Person = &Student{ name: "Tom", age: 18, } fmt.Println(p.getName()) // Tom} Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。 实例化 Student后,强制类型转换为接口类型 Person。

在上面的例子中,我们在 main 函数中尝试将 Student 实例类型转换为 Person,如果 Student 没有完全实现 Person 的方法,比如我们将 (*Student).getName() 删掉,编译时会出现如下报错信息。

1*Student does not implement Person (missing getName method)

但是删除 (*Worker).getName() 程序并不会报错,因为我们并没有在 main 函数中使用。这种情况下我们如何确保某个类型实现了某个接口的所有方法呢?一般可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。

12var _ Person = (*Student)(nil)var _ Person = (*Worker)(nil) 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。 Worker 同上。

实例可以强制类型转换为接口,接口也可以强制类型转换为实例。

123456789func main() { var p Person = &Student{ name: "Tom", age: 18, } stu := p.(*Student) // 接口转为实例 fmt.Println(stu.getAge())} 6.3 空接口

如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。例如

1234567func main() { m := make(map[string]interface{}) m["name"] = "Tom" m["age"] = 18 m["scores"] = [3]int{98, 99, 85} fmt.Println(m) // map[age:18 name:Tom scores:[98 99 85]]} 7 并发编程(goroutine)7.1 sync

Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。

例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。

12345678910111213141516171819202122import ( "fmt" "sync" "time")var wg sync.WaitGroupfunc download(url string) { fmt.Println("start to download", url) time.Sleep(time.Second) // 模拟耗时操作 wg.Done()}func main() { for i := 0; i < 3; i++ { wg.Add(1) go download("a.com/" + string(i+'0')) } wg.Wait() fmt.Println("Done!")} wg.Add(1):为 wg 添加一个计数,wg.Done(),减去一个计数。 go download():启动新的协程并发执行 download 函数。 wg.Wait():等待所有的协程执行结束。 1234567$ time go run .start to download a.com/2start to download a.com/0start to download a.com/1Done!real 0m1.563s

可以看到串行需要 3s 的下载操作,并发后,只需要 1s。

7.2 channel123456789101112131415161718var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道func download(url string) { fmt.Println("start to download", url) time.Sleep(time.Second) ch

上一篇 « 机器学习笔试面试题 11-20 下一篇 » Go Gin 简明教程



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3