Go语言入门

Go语言入门

多学一门语言多条路吧,C/C++易学难精,Go语言原生支持并发编程,现在在微服务服务端编程中也越来越常用了,或许能给只会Java的我提供一个加入鹅厂的机会也说不准呢~

PS:PornHub后端也是用的Go,你懂我的意思吧。

Go语言应用:

  1. Docker
  2. 静态博客框架:Hugo

(〇)Go语言特点

  1. 强类型语言,静态编译,不需要运行时进行类型推导,所以速度比解释型语言(如pythonphp…)快
  2. 存在GC,不需要程序员进行内存回收
  3. 存在未引用变量和未引用包时会报错
  4. Go语言所有编码都是UTF-8,不需要额外指定编码类型

(一)环境搭建

  • 下载并安装对应版本的Go语言包

  • 查看版本:go version

  • 查看环境:go env

    其中GOROOT="/usr/local/go"熟悉就是安装目录。
    GOPATH属性对应的就是Go的项目工作目录,该目录下会有binpkgsrc三个子目录。
    binGo编译好的可运行程序存放地址;
    pkgGo库源码文件;
    srcGo命令源码文件、第三方类库存放地址,相当于根目录,建议将代码放在src目录下

  • 在用户目录下除了GOROOT文件夹外的地方新建一个文件夹,作为GOPATH的值,比如我是/Users/dragonbaby308/go,并创建binpkgsrc三个子目录

  • vim ~/.bash_profile,加:

1
2
3
4
5
GOROOT=/usr/local/go
export GOROOT
export GOPATH=/Users/dragonbaby308/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN:$GOROOT/bin
  • source ~/.bash_profile使之生效

编辑器

  • GoLandJetBrains旗下产品,体验自然没得说
  • Sublime Text安装GoSublime插件
  • 你要是够牛逼,直接用vim-go + coc.nvim(自动补全),10个看了9个都会佩服

(二)源码文件

Go语言源码文件名称都是以.go为后缀,内容以Go语言代码组织的文件。

分类

  1. 命令源码文件:声明自己属于main代码包,包含无参数声明和结果声明的main函数,是Go语言程序的入口

    每个程序只能有一个main包,只有声明了package main的才是命令源码文件,一个程序只允许一个main()

  2. 库源码文件

    除了main的其他package下的都是库源码文件,编译后会在pkg目录下生成.a静态库文件。

  3. 测试源码文件:名称以_test.go为后缀,其中至少有一个函数名称以Test/Benchmark为前缀,且该函数接受一个类型为*testing.T/*testing.B的参数。

    Test:功能测试;
    Benchmark:性能(基准)测试


单元测试小Demo

  1. 新建test包,创建calc.go
1
2
3
4
5
package main

func add(a, b int) int {
return a + b
}
  1. 同目录下创建calc_test.go

    名称以_test.go为后缀,其中至少有一个函数名称以Test/Benchmark为前缀,且该函数接受一个类型为*testing.T/*testing.B的参数

1
2
3
4
5
6
7
8
9
package main

func TestAdd(t *testing.T) {
r := add(2, 4)
if r != 6 {
t.Fatalf("add(2, 4) error, expect: %d, actual: %d", 6, r)
}
t.Logf("test add() success!")
}
  1. 运行calc_test.go,或者在test目录下执行go test -v


(三)基础命令

Go语言的命令和NPM很相似。

1.go run:运行

  • go run用于运行源码文件
  • 只能接受一个命令源码文件以及若干个库源码文件作为文件参数
  • 执行go run会先编译源码文件,存放在一个临时文件夹,然后再运行命令源码文件编译得到的可执行文件、库源码文件编译得到的归档文件

    如果go run不加-work参数,会在命令执行完之前删除临时文件夹

参数

  • -a强制编译相关代码,不论其编译结果是否已经是最新;

  • -n:打印编译过程中所需要运行的命令,但是真正执行;

  • -x:打印编译过程中所需要运行的命令,并且真正执行;

  • -p n并行编译,其中n为并行的数量;

  • -v:列出被编译的代码包的名称;

    -a -v:列出所有(不含标准库)被编译的代码包的名称

  • -work显示编译时创建的临时工作目录的路径,并且在命令执行完后不删除临时工作目录


2.go build:编译

  • 编译.go命令源码文件,在相同目录下得到一个可执行文件。
  • go build不加参数,会将当前目录当作代码包进行编译。

参数

  • -race如果存在资源竞争,运行时会打印在控制台

3.go install:编译并安装

  • go install不加参数,会将当前目录当作代码包进行编译安装。

4.go get:从远程仓库下载并安装

从远程仓库(GitHub/GitLab/Gogs)下载并安装代码包

参数

  • -d:只执行下载动作,而不执行安装操作;
  • -fix:在下载代码包后先执行修正操作,而后再进行编译和安装;
  • -n:利用网络来更新已有的代码包及其依赖包
  • -x:打印编译过程中所需要运行的命令,并且真正执行;

5.ds:查看目录结构

ds for directory structure


6.go test:单元测试

参数

  • -v打印详细信息

7.go mod:模块管理

  • go mod init xxx:在当前目录初始化一个xxx模块,生成go.mod文件,单独进行模块管理,这样项目就不是一定要放在$GOPATH/src目录下了。

    Go语言中系统类库是放在pkg目录下,只有第三方类库才是放在src目录,所以只有需要用到第三方类库的子项目才有go mod的必要性。


(四)数据类型

1.关键字

  • package:声明代码包

  • import:导入代码包

    通过import _ "packagePath"可以在导包时调用对应包的init()方法
    每个包都可以有自己的init()方法,在其中可以写一些初始化资源的默认方法。

  • func:声明函数

  • var:声明变量

  • const:声明常量

  • map:声明K/V对字典

  • chan:声明通道

  • type:定义数据类型

  • struct:定义结构体

  • interface:定义接口

  • go:通过go关键字可以开启一个Goroutine

    Goroutine的本质是协程,你可以理解为轻量级的线程,很多个Goroutine可以映射到一个物理线程上。

  • 程序流控:select/break/case/continue/default/defer/else/fallthrough/for/goto/if/range/return/switch

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

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

2.变量var & 常量const

变量可以只声明不赋值,常量声明了就一定要赋值
Go语言非常严格:声明了变量就一定要使用,否则就会报错

  1. 单行赋值:关键字 名称 类型 ( = 值)/ 名称 := (值)
1
2
3
4
5
var var1 int = 1
var var0 //变量可以只声明不赋值
shortVar := (577) //短变量声明,可以进行类型推断

const cosnt1 int = 1
  1. 平行赋值
1
2
var var2, var3 int = 2, 3
const const2, const3 int = 2, 3
  1. 多行赋值
1
2
3
4
5
6
7
8
9
var (
var4 int = 4
var5 int = 5
)

const (
const4 int = 4
const5 int = 5
)

3.数组

(1)数组的声明

1
2
3
//声明长度为3的数组
type Number1 [3]int
var numbers2 [3]int //不赋值时数组初始值默认为0,即:[3]int{0, 0, 0}

(2)数组的赋值

1
2
3
4
5
6
7
8
9
10
11
//声明定长数组时赋值
var numbers3 [3]int{1, 2, 3}
//声明不定长数组时赋值
var numbers4 [...]int{1, 2, 3}

//声明二维数组,并赋值
//注意每个子数组后都需要加逗号,即使是最后一个子数组
matrix := [2][2]int64{
{0,1},
{2,3},
}

(3)获取数组元素 & 数组长度(len()

1
2
3
4
5
//通过下标获取数组元素,从0开始
numbers3[0] = 4

//数组长度
var length = len(numbers3)

4.切片(不含长度的数组)

(1)切片的声明

  • 声明:type MySlice []int
  • 开辟空间:make([]T, length),其中length为切片长度
1
2
3
4
5
//定义长度为100的int类型的切片
type nums []int = make([]int, 100)

//可以简化为
nums := make([]int, 100)

slice := []int{}定义的是一个空切片,并不是为切片开辟空间!!!

Golang二维切片赋值是真TMD反人类( ̄_ ̄|||)

(2)从数组获取切片

var sliceName = arrayName[sIndex, eIndex]:获得的切片是对应数组的一个[sIndex, eIndex)的子数组(不包含结束位置元素

1
2
3
4
5
6
//数组
var numbers = [5]int{1, 2, 3, 4, 5}
//通过切片表达式获取切片
var slice1 = numbers[1:4] //相当于[3]int{2, 3, 4}
//获取切片的切片
var slice2 = slice1[1:3] //相当于[2]int{3, 4}

(3)从切片获取子切片

注意:子切片和切片共享底层数据结构,修改子切片对原切片是可见的。如果想要修改切片内容,需要复制一份再进行更改

1
2
3
4
sub1 := slice[i:j]  //i到j,不包括j
sub2 := slice[i:] //i到尾部
sub3 := slice[:j] //头部到j,不包括j
sub4 := slice[:] //相当于复制整个切片

(4)切片的长度 & 容量(cap()

  • 切片的长度:eIndex - sIndex
  • 切片的容量:len - sIndex,其中len切片来源数组的长度
  • 未赋值的切片默认容量为nil
1
2
3
4
5
//capacity = len - 1
//len为来源数组长度,对于切片的子切片来说,是其父切片的容量,即cap(slice1)
//cap(slice1) = len(numbers) - 1 = 4
//capacity = cap(slice1) - 1 = 3
var capacity int = cap(slice2) //值为3

(5)添加元素:append()

  • append(slice []Type, elems ...Type) []Type
1
2
3
4
5
6
var numbers2 []int
numbers2 = append(numbers2, 0)

//append()也可以连接两个切片
numbers3 := []int{1, 2, 3}
numbers2 = append(numbers2, numbers3...) //需要使用...对切片进行解包

(6)遍历:for idex, s := range slice{}

1
2
3
4
5
6
7
8
9
s := []string{"a", "b", "c", "d"}
for idx, item := range(s) {
fmt.Println(idx, item)
}

//如果想省略下标
for _, item := range s {
fmt.Println(item)
}

5.字典

特点:

  1. Go语言的map无序的!业务如果要求有序,需要自己实现
  2. Go语言的map非并发安全的!需要进行额外的同步

格式:map[K]V

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mm := map[int]string{1:"a", 2:"b", 3:"c"}
b := mm[2]
//删除:有则删除,没有则无视
delete(mm, 4)
//对于字典值来说,如果其中不存在索引表达式欲取出的键值对,那么就以它的值类型的空值(或称默认值)作为该索引表达式的求值结果
e := mm[5]
//e的值一定是空字符串"",那么我们怎么知道它是本来就为空,还是不存在该索引值呢?
//字典的索引表达式可以有两个结果,第二个结果是bool类型的,表明该键是否存在
e, ok := mm[5]

//map的value声明成空接口类型,说明value可以是任何类型
var m map[string]interface{}
//通过make为map初始化
m = make(map[string]interface{})
//赋值
m["username"] = "user2"

遍历:for-range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取所有K/V
for k, v := range(mm) {
//因为GoLang的map是无序的,所以每次打印的顺序是不确定的
fmt.Printf("m[%s]:%d\n", k, v)
}

//只获取所有Key
for k := range mm {
fmt.Println(k)
}

//只获取所有Value
for _, v := range(mm) {
fmt.Println(v)
}

通过map实现setmap[k]bool

Go本身没有实现set数据结构,我们可以通过map[k]bool来进行去重:

1
2
3
4
5
6
7
8
m := make(map[string]bool)
m["hello"] = true
m["world"] = true
key := "hello"
//ok为true时,对应key存在,否则不存在
if _, ok := m[key]; ok{
fmt.Printf("%s key exists\n", key)
}

6.通道【Go并发编程的基础】

参考Go语言并发编程》

  • 通道(Channel)的一种特殊的数据结构,用于在不同Goroutine之间传递类型化的数据,并且是并发安全的

    Goroutine(也称为Go程序,本质上是协程)可以被看成是承载可被并发执行的代码块的载体,它们Go语言的运行时系统调度,并且依托于操作系统线程来并发执行其中的代码
    Go运行时调度机制

  • 声明通道:chan T,如chan int

  • 赋值:make(chan T, length),其中length是通道的长度,如make(chan int, 5)

    length可以省略,即make(chan T)

  • 接收/发送数据:<-

  • 关闭通道:close()

    重复关闭通道/向已关闭的通道发送数据,都会造成异常

1
2
3
4
5
6
7
8
9
//赋值
channel := make(chan string, 5)
ch := make(chan int)
//发送数据到通道
channel <- "value1"
//从通道接收数据,第二个值是bool类型的,true代表通道值有效
value, ok := <- channel
//关闭通道
close(channel)

7.函数

  • Go语言中函数是一等公民,可以作为值来传递和使用,参考Java8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//将函数赋值给变量
myPrint := func(s string) {
fmt.Println(s)
}
//通过变量使用函数
myPrint("param") //param


//函数还可以作为map的value,通过传入字符串动态选择使用哪个函数
funcMap := map[string]func(int, int) int{
"add" : func(a int, b int) int {
return a + b
},
"sub" : func(a int, b int) int {
return a - b
},
}
fmt.Println(funcMap["add"](3,2)) //5
fmt.Println(funcMap["sub"](3,2)) //1
  • 可以简单地认为:函数名大写是public函数,可以在包外被访问到;函数名小写是private函数,不能在包外被访问到

  • Go语言函数不支持泛型,没有默认参数。

  • 函数定义:func functionName(入参列表) (出参列表){},如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func sum0(a int, b int) int {
return a + b
}

//如果多个参数类型一致,可以只写一个
func sum1(a, b int) int {
return a + b
}

//我们可以给返回值命名,这时候需要使用赋值的方式返回结果,
//而且return可以不带返回值
//如果出参只有一个,可以省略括号
//出参中的参数名称也是可以省略的,只保留参数类型
func sum2(a, b int) (res int) {
res = a + b
return
}

//Golang也支持可变参数列表,实质上就是切片
func sum3(init int, vals ...int) int {
sum := init
for _, v := range vals{
sum += v
}
return sum
}
//调用方法
//1.直接通过多个参数调用:sum3(0,1,2,3),结果为6
//2.传入【解包后的】切片:sum3(0, []int{1,2,3}...),结果为6
  • Go语言中的函数可以有多个返回值,常用于返回错误
1
2
3
4
5
6
7
8
9
10
11
func sum4(init int, vals ...int) (int int) {
sum := init
for _, v := range vals{
sum += v
}
return sum, len(vals)
}

//获取返回值,赋值给变量后打印
sum, length := sum4(0, []int{1,2,3}...)
fmt.Println(sum, length)

(1)参数传递:值传递/引用传递

  1. 值传递:和Java类似,Go语言函数中,传递的参数如果是内置类型(比如数值类型、字符串、布尔类型、数组),会采用值传递 —— 将参数值拷贝一份后传入函数中,在函数中对传入的参赛进行修改,不会影响原参数的值
1
2
3
4
5
6
7
8
9
10
11
12
13
func chgStr(s string)  {
s = "chgStr"
fmt.Printf("chgStr中将字符串s的值改为了: %s\n", s)
}

func main() {
s := "origin string"
chgStr(s)
fmt.Printf("chgStr函数外,字符串s的值为: %s\n", s)
}
//输出结果:
//chgStr中将字符串s的值改为了: chgStr
//chgStr函数外,字符串s的值为: origin string

  1. 对于引用类型(slice/map/chan/interface/func)来说,它们通过底层指针unsafe.Pointer共享存储,所以修改入参会改变原有参数的值,但实际上仍然是值传递:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//type slice struct {
// array unsafe.Pointer
// len int
// cap int
//}

func chgMap(m map[string]string) {
m["Go"] = "值传递"
}

func main() {
m := map[string]string{"Java":"值传递"}
chgMap(m)
fmt.Println(m)
}
//输出结果:
//map[Go:值传递 Java:值传递]

  1. 引用传递(*/&):对于字符串/数字/结构体,我们还可以通过传递指针对原参数进行修改:
1
2
3
4
5
6
7
8
9
10
11
12
//入参为指针
func chgStrByPtr(s *string) {
*s = "DragonBaby"
}

func main() {
name := "yxl"
chgStrByPtr(&name) //通过取址符&传递指针
fmt.Println(name)
}
//输出结果:
//DragonBaby

(2)匿名函数

1
2
3
4
5
6
7
func main() {
func(s string) {
fmt.Printf("匿名函数入参:%s \n", s)
}("param")
}
//输出:
//匿名函数入参:param

(3)函数式编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func FilterIntSlice(ints []int, predicate func(i int) bool) ([]int) {
res := make([]int, 0)

//遍历切片
for _, v := range ints {
//判断是否满足谓词函数
if predicate(v) {
//满足条件的加入结果切片
res = append(res, v)
}
}

return res
}

func main() {
ints := []int{1,2,3,4,5}
odd := func(i int) bool { return i % 2 == 1}
fmt.Println(FilterIntSlice(ints, odd))
}

8.结构体

  • 定义结构体:tpye StructName struct{若干字段声明}
1
2
3
4
5
6
7
8
9
10
11
//定义结构体
type Person struct{
Name string
Gender string
Age uint8
}

//创建结构体
Person{Name:"Robert", Gender:"Male", Age:30}
//如果键值对的顺序与其类型中的字段声明完全相同的话,我们还可以统一省略掉所有字段的名称
p := Person{"Lee Tang", "Male", 20}

每个字段声明后可以带标签,参考第六节的Demo

结构体的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (person *Person) Grow() {
person.Age++
}

func (person *Person) Move(newAddress string) string {
old := person.Address
person.Address = newAddress
return old
}

//调用结构体的方法
p.Grow()

//通过指针创建结构体
person := &Person{
Name: "阿里",
Gender: "男",
Age: 20,
}

9.接口

  • 定义接口:tpye InterfaceName interface{若干方法声明}
1
2
3
4
type Animal interface{
Grow()
Move(string) string
}
  • 如果一个数据类型所拥有的方法集合中包含了某一个接口类型中的所有方法声明的实现,那么就可以说这个数据类型实现了那个接口类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//builtin.go中的error接口
type error interface {
Error() string
}

//定义一个结构体
type PathError struct{
path string
op string
time string
msg string
}

//使用该结构体的指针,实现某个接口的全部方法,就可以认为是实现了该接口
func (p *PathError) Error() string {
return fmt.Sprintf("自定义异常:\n path=%s\n op=%s\n time=%s\n msg=%s\n", p.path, p.op, p.time, p.msg)
}
  • 如果想让一个方法能够接收/返回任何类型的参数,就将参数声明为空接口v interface{} —— 因为任何类型都可以是空接口

10.指针

  • &:取址,当&应用到一个值上会取出指向该值的指针值

    凡是需要改变一个对象的内存地址的,都需要使用&取址
    比如说,改变一个map中某个keyvalue不需要&,但是反序列化时为一个新的map分配内存空间就需要用到&取址,见(七)-3-(2)-①~③

  • *:取值,当*应用到一个指针值上会取出该指针指向的值


11.错误(error):返回错误值

(1)err错误 + defer

Go提倡认真对待每一个错误,所以它没有采用try-catch的异常机制,而是采用返回错误值的形式。一般来说可能返回错误的函数,通过一个err对象接收后直接打印或使用即可。

1
2
3
4
5
6
7
8
9
f, err := os.Open(file)

//处理错误
if err != nil {
return err
}

//通过defer语句,对打开的资源比如文件进行显式的关闭
defer f.Close()

(2)简单字符串错误

首先看下Go语言中简单字符串错误的类库errors.go源码:

1
2
3
4
5
6
7
8
9
10
11
package errors

func New(text string) error {
//返回一个错误信息结构体,使用的是传入的字符串
return &errorString{text}
}

//错误信息结构体,只包含一个字符串
type errorString struct{
s string
}

所以说想要自定义简单字符串错误,使用errors.New(text string)即可:

1
2
3
4
err1 := errors.New("简单字符串错误")
fmt.Println(err1)
//输出:
//简单字符串错误

(3)接口错误

builtin.go类库下存在error接口,源码如下:

1
2
3
4
5
package builtin

type error interface{
Error() string
}

通过实现error接口即可实现自定义接口错误,在Go中,实现了一个接口的全部方法就可以认为是实现了该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//定义结构体
type PathError struct {
path string
op string
time string
msg string
}

//实现error接口
func (p *PathError) Error() string {
return fmt.Sprintf("自定义错误:\n path=%s\n op=%s\n time=%s\n msg=%s\n", p.path, p.op, p.time, p.msg)
}

//OpenFile函数,异常时返回一个PathError结构体,实现了error接口
func OpenFile(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return &PathError{
path: filePath,
op: "read",
time: time.Now().String(),
msg: err.Error(),
}
}
defer file.Close()
return nil
}

func main() {
err2 := OpenFile("C:/faaaaaa/gaaaaa/donotexistfile.txt")
if err2 != nil {
fmt.Println(err2)
}
}
//输出:
// 自定义错误:
// path=C:/faaaaaa/gaaaaa/donotexistfile.txt
// op=read
// time=2019-12-03 15:28:14.7397259 +0800 CST m=+0.001979701
// msg=open C:/faaaaaa/gaaaaa/donotexistfile.txt: The system cannot find the path specified.
switch判断错误类型
1
2
3
4
5
6
switch v := err.(type) {
case *PathError:
fmt.Println("PathError:", v)
default:
fmt.Println(v)
}

12.异常panic() & 捕获recover()

  • error是不严重的错误,而panic()会直接退出进程,只有特别严重的错误才会使用panic()
  • panic()发生后,通过recover()可以进行异常捕获,并将其包装为error,从而使进程继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

func main() {
fmt.Println("发生异常前")

//声明异常捕获方法
defer func() {
if err := recover(); err != nil {
fmt.Println("异常捕获:", err)
}
}()

//可能抛出异常的方法
Panic()

fmt.Println("发生异常后")
}

//抛出异常的方法
func Panic() {
var x = 30
var y = 0
//自定义异常
panic("抛出自定义异常!")
var c = x / y
fmt.Println(c)
}
//输出:
// 发生异常前
// 异常捕获: 抛出自定义异常!

(五)流程控制

1.if

1
2
3
4
5
6
7
8
//else if 和 else可以没有
if 100 > number {
number += 3
} else if 100 < number {
number -= 2
} else {
fmt.Println("100 hun!")
}

if语句的初始化子句

  • 我们可以if子句中为变量赋值,不需要单独赋值
  • 初始化子句要放在if; 条件表达式中间
  • 初始化子句中定义的变量在if语句外是不可见的,除非在if语句外进行了声明
1
2
3
4
5
6
7
8
9
10
11
12
//number的作用域只在if内
if number := 4; 100 > number {
//...
}

m := make(map[string]string)
m["key1"] = "value1"
if v, ok := m["key1"]; ok{
fmt.Println(v)
}else{
fmt.Println("key not exists")
}

2.switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
names := []string{"Golang", "Java", "Rust", "C"}

//switch也可以有初始化子句
switch name := names[0]; name {
case "Golang":
fmt.Println("Tencent/PornHub")
case "Java":
fmt.Println("Alibaba")
default:
fmt.Println("回家喂猪是一样的")
}

//switch语句后表达式可以为空
//case语句可以是表达式
a,b := 1,2
switch {
case a < b:
fmt.Println("a < b")
case a >= b:
fmt.Println("a >= b")
}

3.for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for i := 0; i < 10; i++ {
fmt.Print(i, " ")
}

//for-range语句
for i, v := range "fxxk" {
fmt.Printf("%d: %c\n", i, v)
}
// 0: f
// 1: x
// 2: x
// 3: k

//Go语言中声明了i就一定要使用,如果不想打印,可以使用_代替
for _, v := range "fxxk" {
fmt.Printf("%c\n", v)
}
//f
//x
//x
//k

//死循环
for {
fmt.Println("死循环")
}

4.select:用于选择不同通道接收数据

1
2
3
4
5
6
7
8
9
10
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
select {
case e1 := <-ch1:
fmt.Printf("1th case is selected. e1=%v.\n", e1)
case e2 := <-ch2:
fmt.Printf("2th case is selected. e2=%v.\n", e2)
default:
fmt.Println("No data!")
}

5.defer:延迟函数执行,在函数执行完成后调用

类似于Java中的try-with语句,或者python中的with语法。


用途

  1. 显式关闭资源,如文件
1
2
3
4
5
6
7
8
9
func readFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
//关闭文件
defer file.Close()
return ioutil.ReadAll(file)
}
  1. 释放锁
1
2
mu.Lock()
defer mu.UnLock()

执行顺序:FILO

defer在函数体执行完后执行,在函数返回之前执行;并且遵循FILO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

func testDefer() string {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
fmt.Println("函数体")
return "返回值"
}

func main() {
fmt.Println(testDefer() )
}
//输出:
//函数体
//defer 2
//defer 1
//返回值

(六)网络应用Demo

参考Go语言网络编程》

  1. 创建main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"net/http" //生产级别的HTTP服务器,具备抵御常见攻击的能力
"encoding/json" //JSON格式解析
"strings" //处理字符串的库
)

func main() {
//让hello函数处理来自"/hello"的请求
//每当一个新请求进入到与路径匹配的HTTP服务器时,服务器将生成一个执行hello()的新goroutine
http.HandleFunc("/hello", hello)
//让weather函数处理来自"/weather"的请求
//每当一个新请求进入到与路径匹配的HTTP服务器时,服务器将生成一个执行weather()的新goroutine
//使用匿名函数
http.HandleFunc("/weather/", func(w http.ResponseWriter, r *http.Request) {
city := strings.SplitN(r.URL.Path, "/", 3)[2]
//Go中的函数可以接收两个返回值,一般都会通过err判断是否发生异常
data, err := query(city)
//异常处理
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//设置HTTP响应头
w.Header().Set("Content-Type", "application/json; charset=utf-8")
//JSON格式化
json.NewEncoder(w).Encode(data)
})

//在8080端口启动服务器
http.ListenAndServe(":8080", nil)
}

//hello函数,用于处理来自"/"的请求
//http.ResponseWriter:用于发送响应报文到客户端,参数为byte类型的切片
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello!"))
//如果觉得使用byte数组不直观,可以通过fmt.Fprintf()进行输出
//fmt.Fprintf(w, "hello!")
}

//query函数
//回参可以省略参数名,只保留类型,weatherData是一个结构体类型
func query(city string) (weatherData, error) {
//API_KEY需要申请
resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?APPID=YOUR_API_KEY&q=" + city)
//异常处理
if err != nil {
return weatherData{}, err
}
//延迟处理函数,用于关闭资源
defer resp.Body.Close()
var d weatherData
//if初始化子句,通过&d指针给结构体塞值,err只有发生了异常才会有值
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
return weatherData{}, err
}
return d, nil
}

//天气数据结构体
type weatherData struct {
Name string `json:"name"` //在字段后加标签,方便进行JSON解析
Main struct {
Kelvin float64 `json:"temp"`
} `json:"main"`
}
  1. 运行:go run
  2. 测试:
1
2
3
4
5
curl http://localhost:8080/hello
//输出:hello!

curl http://localhost:8080/weather/tokyo
//输出:{"name":"Tokyo","main":{"temp":295.9}}

(七)常用的库

已经使用过的fmtnet/httpstrings不再记录。

1.sort:排序

  1. sort.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"sort"
"fmt"
)

func main() {
a := []int{3, 6, 2, 1, 9, 10, 8}
//sort.Ints(),排序int类型切片/数组
sort.Ints(a)

for _, v := range a {
fmt.Println(i, v)
}
}
  1. 输出:
1
2
3
4
5
6
7
1
2
3
6
8
9
10

2.time:时间

(1)停顿:time.Sleep()

1
2
3
4
5
6
import "time"

func main() {
time.Sleep(1*time.Millisecond) //停顿1ms
time.Sleep(5*time.Second) //停顿5s
}

(2)一次性计时器:time.After()

  • 一次性计时器触发后会被GC,但是不保证何时回收,如果对性能要求高的话,不推荐使用time.After,应该用time.NewTicker()取代,手动关闭
  • 一次性计时器可以用于超时控制
1
2
3
4
5
6
7
8
9
10
import "time"

func main() {
//1s后触发一次
select {
case v := <- ch: fmt.Println("正常情况")
//不推荐使用!
case <- time.After(time.Second) : fmt.Println("超时1s")
}
}

(3)定时器:time.NewTicker()

定时器也可以用于超时控制,但是一定要关闭定时器,否则可能造成内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import "time"

func main() {
//每过1s触发一次
t := time.NewTicker(time.Second)

//定时器也可以用于超时控制
select{
case v := <- ch: fmt.Println("正常情况")
//t.C是一个时间的通道
case <- t.C : fmt.Println("超时1s")
}

//每过1s打印一次
for v := range t.C {
fmt.Println("hello", v)
}

//关闭定时器
t.Stop()
}
//输出:
// 超时1s
// hello 2019-12-04 14:39:27.2520634 +0800 CST m=+2.005214701
// hello 2019-12-04 14:39:28.251665 +0800 CST m=+3.004824301
// hello 2019-12-04 14:39:29.2532157 +0800 CST m=+4.006383001
//……

3.encoding/json

(1)序列化

  1. NewEncoder(w io.Writer).Encode(v interface{})

    参考(六)网络应用Demo

  2. json.Marshal(v interface{}) ([]byte, error):传入任何类型的参数,转换为byte[]类型的JSON数组

①结构体序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"encoding/json"
"fmt"
)

//定义用户结构体
type User struct {
UserName string `json:"user_name"`
NickName string `json:"nick_name"`
Age int //留一个不加标签,方便看标签的作用
Birthday string `json:"birthday"`
Sex string `json:"sex"`
Email string `json:"e-mail"`
Phone string `json:"phone"`
}

func main() {
//创建结构体对象
user := &User{
UserName: "user001",
NickName: "你弟含王",
Age: 14,
Birthday: "1988-08-08",
Sex: "男",
Email: "741@qq.com",
Phone: "741741741",
}
jsonBytes, err := json.Marshal(user)
if err != nil {
fmt.Printf("json.Marshal err: ", err)
}
fmt.Printf("%s\n", jsonBytes)
}
//输出:
//{"user_name":"user001","nick_name":"你弟含王","Age":14,"birthday":"1988-08-08","sex":"男","e-mail":"741@qq.com","phone":"741741741"}
//可以看到没有打标签的,JSON序列化时key就是字段名,打了标签则key是标签名

②字典序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"encoding/json"
"fmt"
)

func main() {
//定义[string, interface{}]类型的map
var m map[string]interface{}
//初始化
m = make(map[string]interface{})
m["username"] = "user002"
m["age"] = 18

jsonBytes, err := json.Marshal(m)
if err != nil {
fmt.Printf("json.Marshal err: ", err)
}
fmt.Printf("%s\n", jsonBytes)
}
//输出:
//{"age":18,"username":"user002"}

③切片序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"encoding/json"
"fmt"
)

func main() {
//切片由[string, interface{}]的map组成
var s []map[string]interface{}

//定义并给map赋值
var m map[string]interface{}
m = make(map[string]interface{})
m["nickname"] = "小老弟"
m["address"] = "聚宝山庄"

//将map添加到切片
s = append(s, m)

jsonByteArrays, err := json.Marshal(s)
if err != nil {
fmt.Printf("json.Marshal() err: ", err)
return
}
fmt.Printf("%s\n", string(jsonByteArrays))

}
//输出:JSON数组
//[{"address":"聚宝山庄","nickname":"小老弟"}]

(2)反序列化

  1. NewDecoder(r io.Reader).Decode(v interface{})

    参考(六)网络应用Demo

  2. json.Unmarshal(data []byte, v interface{}) error第二个参数传入的要是&指针引用

①结构体反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"encoding/json"
"fmt"
)

//定义用户结构体
type User struct {
UserName string `json:"user_name"`
NickName string `json:"nick_name"`
Age int //留一个不加标签,方便看标签的作用
Birthday string `json:"birthday"`
Sex string `json:"sex"`
Email string `json:"e-mail"`
Phone string `json:"phone"`
}

func main() {
//创建结构体对象
user := &User{
UserName: "user001",
NickName: "你弟含王",
Age: 14,
Birthday: "1988-08-08",
Sex: "男",
Email: "741@qq.com",
Phone: "741741741",
}
//获取转换后的JSON字节数组
jsonBytes, err := json.Marshal(user)
if err != nil {
fmt.Printf("json.Marshal err: ", err)
}

//反序列化
var userResult User
//结构体不是引用类型,所以此处传入的必须是指针引用,否则会报错
err = json.Unmarshal(jsonBytes, &userResult)
if err != nil {
fmt.Println("json.Unmarshal err: ", err)
}else {
fmt.Println(userResult)
fmt.Println(userResult.Age)
}
}
//输出:
//{user001 你弟含王 14 1988-08-08 男 741@qq.com 741741741}
//14

②字典反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"encoding/json"
"fmt"
)

func main() {
//序列化
var m map[string]interface{}
m = make(map[string]interface{})
m["username"] = "user002"
m["age"] = 18
jsonBytes, err := json.Marshal(m)
if err != nil {
fmt.Println("json.Marshal err: ", err)
}

//反序列化
var mapResult map[string]interface{}
mapResult = make(map[string]interface{})
//传入的需要是&指针引用
err = json.Unmarshal(jsonBytes, &mapResult)
if err != nil {
fmt.Println("json.UnMarshal err: ", err)
}
fmt.Println(mapResult)
}
//输出:
//map[age:18 username:user002]

③切片序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"encoding/json"
"fmt"
)

func main() {
//序列化
var s []map[string]interface{}
var m map[string]interface{}
m = make(map[string]interface{})
m["nickname"] = "小老弟"
m["address"] = "聚宝山庄"
s = append(s, m)
jsonByteArrays, err := json.Marshal(s)
if err != nil {
fmt.Printf("json.Marshal() err: ", err)
return
}

//反序列化
var sliceResult []map[string]interface{}
err = json.Unmarshal(jsonByteArrays, &sliceResult)
if err != nil {
fmt.Printf("json.UnMarshal() err: ", err)
}
fmt.Println(sliceResult)

}
//输出:
//[map[address:聚宝山庄 nickname:小老弟]]

4.runtime:获取Go运行时环境

1
2
3
4
5
6
7
8
9
import runtime

//获取运行时CPU核心数
cpus := runtime.NumCPU()
fmt.Println(cpus)

//获取运行中的Goroutine数
gos := runtime.NumGoroutine()
fmt.Println(gos)

5.sync:同步

(1)sync.Mutex:互斥锁

1
2
3
4
5
6
7
8
9
10
//定义互斥锁
var lock sync.Mutex

//获取锁
lock.Lock()

//同步代码块……

//释放锁
lock.Unlock()

(2)sync.RWMutex:读写锁

适合读多写少的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义读写锁
var lock sync.RWMutex

//获取写锁
lock.Lock()

//同步代码块……

//释放写锁
lock.Unlock()

//获取读锁
lock.RLock()
//释放读锁
lock.RUnlock()

6.ioutil:I/O工具类

  • ioutil.ReadAll(r io.Reader) ([]byte, error):传入一个io.Reader,以字节数组的形式返回其中内容
1
2
3
4
5
6
7
8
response, err := http.Get("http://www.dragonbaby308.com/")
if err != nil {
fmt.Println("http.Get()错误:", err)
return
}

//Body io.ReadCloser
data, err := ioutil.ReadAll(response.Body)

7.math:数学库

比较float64类型的大小

由于float64类型的数据具备精度的概念,开发者不具备精确比较两个float64类型数据的能力,所以Golang提供了库函数;
但是对于其他的int类型数据,由于大小比较实现起来并不难,Golang为了保证语言的尽可能精炼,不提供比较两个int类型数据大小的库函数,需要开发人员自己实现(而且Golang还不支持?:的三元运算符,反正我觉得是挺没道理的━┳━ ━┳━

  • math.Min(x, y float64) float64:两数小者
  • math.Max(x, y float64) float64:两数大者
-------------本文结束感谢您的阅读-------------

本文标题:Go语言入门

文章作者:DragonBaby308

发布时间:2019年11月20日 - 23:03

最后更新:2020年02月24日 - 11:19

原始链接:http://www.dragonbaby308.com/go/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

急事可以使用右下角的DaoVoice,我绑定了微信会立即回复,否则还是推荐Valine留言喔( ఠൠఠ )ノ
0%