Go 语言入门指南:基础语法和常用特性解析| 青训营

66 阅读15分钟

适用于有面向对象语言基础

go的基本结构

Go语言的基本结构包括以下几个部分:

  1. 包(Package):Go语言的代码是以包的形式组织的,每个Go文件都属于一个包。包可以是自己定义的,也可以是标准库或第三方库提供的。
  2. 导入(Import):通过导入其他包,可以使用其他包中的函数、变量和类型。使用关键字import来导入包。
  3. 函数(Function):Go语言的程序执行入口是main函数,每个可执行程序必须包含一个main函数。除了main函数外,还可以定义其他函数来实现具体的功能。
  4. 变量(Variable):在Go语言中,需要先声明变量,然后才能使用。可以使用关键字var来声明变量,也可以使用短变量声明方式:=来声明并初始化变量。
  5. 控制语句(Control Statements):Go语言提供了常见的控制语句,如条件语句(if-else)、循环语句(for、while)、选择语句(switch)等,用于控制程序的执行流程。
  6. 数据类型(Data Types):Go语言支持多种基本数据类型,包括整型、浮点型、布尔型、字符串等。还可以使用结构体、数组、切片、映射等复合数据类型。
  7. 指针(Pointer):Go语言支持指针类型,可以通过指针来间接访问和修改变量的值。
  8. 结构体(Struct):结构体是一种自定义的复合数据类型,可以包含多个字段,每个字段可以是不同的数据类型。
  9. 方法(Method):Go语言中的方法是一种特殊的函数,与某个类型关联,可以在该类型的实例上调用。
  10. 接口(Interfa型。类ce):接口定义了一组方法的集合,实现了这些方法的类型就是该接口的实现
  package main
//源文件中非注释的第一行指明这个文件属于哪个包

  import "fmt"
//导包
/* 多个包可以使用如下语法
  import{
  	fmt
    XXX
  }
*/

  func main() {
          /* 简单的程序 万能的hello world */
          fmt.Println("Hello Go")
  }

变量的声明

局部变量的声明:

⽅法⼀:声明⼀个变量 默认的值是0 var a int

⽅法⼆:声明⼀个变量,初始化⼀个值 var b int = 100

⽅法三:在初始化的时候,可以省去数据类型,通过值⾃动匹配当前的变量的数据类型 var c = 100

⽅法四:(常⽤的⽅法) 省去var关键字,直接⾃动匹配 e := 10

全局变量的声明:

除上述方法四之外都可以

多变量的声明:

var xx, yy int = 1, 2

var aa, bb = 10086, "judy"

var ( //一般用于全局变量的声明
    a int = 1
    b string = "judy"
    c bool = true
)

常量的声明:

加入关键字const

const a int = 1

const (
 a = 1
 b = 2
)

iota标示符

作用:可以用于增长数字的定义,可以用于表达式,在常量中的存储结果值。

函数

基本语法

函数的基本组成为:关键字 func、函数名、参数列表、返回值、函数体和返回语句

注意:go语言的函数支持返回多个值

func 函数名(形参) 返回值{
    内容
}

func swap(x, y string) (string, string) {
   return y, x
}

执行顺序

golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。

虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。

go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。

如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。

当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。

等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:

值传递与引用传递

go语言支持指针

*指针

&取地址符

defer

类似于java中的finally

常用于

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

func Demo(){
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	defer fmt.Println("4")
}
// 4 3 2 1

数组 & slice & map

与java的比较:

  1. 切片(Slice&集合):
  • 在Go语言中,slice是一种动态大小的数据结构,类似于动态数组。它可以通过索引访问元素,并且可以自动扩展以容纳更多的元素。在Java中,ArrayList是一个类似于slice的数据结构,它可以动态地添加和删除元素。
  1. 映射(Map):
  • Go语言的映射是一个键值对集合,类似于哈希表或字典。映射是无序的,键必须是唯一的。
  • Java中的映射接口是java.util.Map,实现该接口的常见类有java.util.HashMap和java.util.TreeMap。它们也提供了键值对的存储,但Java的映射是有序的,且键必须是唯一的。

数组

数组的声明

//固定长度的数组
var myArray1 [10]int
myArray2 := [10]int{1,2,3,4}
myArray3 := [4]int{11,22,33,44}

注意:固定⻓度的数组在传参的时候, 是严格匹配数组类型的

func printArray(myArray [4]int) { //这点和java不同
	//值拷贝
}

slice切片

类似于动态数组, 切⽚的扩容机制,append的时候,如果⻓度增加后超过容量,则将容量增加2倍

切片可以通过使用make函数来创建,例如slice := make([]int, length, capacity),其中length是切片当前的长度,capacity是切片底层数组的容量。也可以通过类型转换将一个数组转换为切片,例如slice := []int(array)

切片与底层数组是分离的,切片操作不会修改底层数组。切片的长度是当前元素在底层数组中的索引,而容量则是底层数组从切片的起始索引到数组末尾的长度。

切片的定义:

var slice0 []type 一个切片在未初始化之前默认为 nil,长度为 0

var slice1 []type = make([]type, len)

slice1 := make([]type, len)

slice2 :=make([]int,len,cap)//cap指定容量 len 是数组的长度并且也是切片的初始长度

切片的初始化:

声明slice1是一个切片,并且初始化,默认值是1,2,3。 长度len是3 slice1 := []int{1, 2, 3}

声明slice1是一个切片,但是并没有给slice分配空间 var slice1 []int slice1 = make([]int, 3) //开辟3个空间 ,默认值是0

声明slice1是一个切片,同时给slice分配空间,3个空间,初始化值是0 var slice1 []int = make([]int, 3)

声明slice1是一个切片,同时给slice分配空间,3个空间,初始化值是0, 通过:=推导出slice是一个切片 slice1 := make([]int, 3)

切片相关函数

  • len() 函数:获取长度。
  • cap() 函数:测量切片最长可以达到多少。

切⽚的⻓度和容量不同,⻓度表示左指针⾄右指针之间的距离,容量表示左指针⾄底层数组末尾的距离。

  • append()函数:追加新元素。如果当前容量不足,底层数组会自动扩容。切片的容量可以多次扩容,每次扩容容量会翻倍。
  • copy()函数:拷贝切片。将原切片的内容复制到新的切片中,不会改变切片的长度和容量。
/* 添加多个元素 */
   numbers = append(numbers, 2,3,4)

/* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)

可以通过delete函数删除切片中的元素,例如slice = append(slice[:index-1], slice[index+1:]...)。

切片可以使用==运算符进行比较,比较的是切片的长度、容量和底层数组的内容是否相同。

map

类似于java中的map<key,value>

声明方式

映射可以通过make函数来创建,例如myMap := make(map[keyType]valueType),其中keyType是键的类型,valueType是值的类型。也可以通过直接赋值来创建一个空的映射,例如myMap := map[keyType]valueType{}

使用方式

  1. 映射的访问:
    可以通过键来访问映射中的值,例如value := myMap[key]。如果键不在映射中,将返回该类型的零值。
  2. 映射的修改:
    可以通过键来修改映射中的值,例如myMap[key] = newValue。如果键不存在,会将该键添加到映射中,并将其值设置为指定值。
  3. 映射的删除:
    可以使用delete函数删除映射中的键及其对应的值,例如delete(myMap, key)
  4. 映射的遍历:
    可以使用range关键字遍历映射的键值对,例如for key, value := range myMap { ... }
//增删改查
val, key := language["php"]  //查找是否有php这个子元素
if key {
    fmt.Printf("%v", val)
} else {
    fmt.Printf("no");
}

language["php"]["id"] = "3" //修改了php子元素的id值
language["php"]["nickname"] = "hello" //增加php元素里的nickname值
delete(language, "php")  //删除了php子元素

面向对象特征

继承

结构体

在面向对象编程中,我们定义类(class)来描述具有相似属性和行为的对象。然后创建该类的实例,即对象。在Go语言中,我们使用结构体来定义类,并通过结构体的方法来实现对象的行为。

类似于java中的类class

如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问

//如果类名首字母大写,表示其他包也能够访问
type Hello struct {
	Name  string
	age   int
}

this关键字

与java中的基本相同

父类

子类可以重写父类的方法,也可以调用父类的方法和自己的方法

java中是extend 父类;go中在结构体里声明就行了

type Teacher struct {
    Human//父类
    major string
}

子类

定义子类对象

//s := Teacher{Human{"judy", "female"}, "语文"}
	var s Teacher
	s.name = "judy"
	s.sex = "male"
	s.major = “数学”

多态

接口

接口的定义

接口的本质是指针

type AnimalIF interface {
	Sleep() //睡觉方法
	GetNumber() string //获取动物的数量
}

实现接口

重写全部方法即可实现此接口

var animal AnimalIF //接口的数据类型
animal = &Cat{"3"}
animal.Sleep() //调用Cat的Sleep()方法

空接口interface{}

类似于java的object, 可以⽤interface{}类型 引⽤ 任意的数据类型

所有的基本类型都实现了此接口

类型断言

如果断言失败,那么ok的值将会是false,但是如果断言成功ok的值将会是true,同时value将会得到所期待的正确的值。

str := "hello"
value, ok := str.(string)

反射

变量结构

Golang关于类型设计的一些原则

  • 变量包括(type, value)两部分
  • type 包括 static typeconcrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete typeruntime系统看见的类型
  • 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:pair(value,type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

-reflect包-

TypeOf & ValueOf

ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0

TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

  1. reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 "Allen.Wu" 25} 这样的结构体struct的值
  3. 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

结构体标签

type Movie struct {
	Title  string   `json:"title"`
	Year   int      `json:"year"`
	Price  int      `json:"rmb"`
	Actors []string `json:"actors"`
}

可以通过反射通过key获取value

主要可以用来json格式编码与解码

//编码的过程  结构体---> json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return
	}
//解码的过程 jsonstr ---> 结构体
	myMovie := Movie{}
	err = json.Unmarshal(jsonStr, &myMovie)
	if err != nil {
		fmt.Println("json unmarshal error ", err)
		return
	}

goroutine

goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

go func() {
    // 执行任务
}()

使用defer语句来注册一个函数,该函数会在goroutine退出时自动执行。

func main() {
    go func() {
        defer func() {
            // 执行任务2
        }()
        // 执行任务1
    }()
}

调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。


特点:

  1. Goroutine可以与普通的函数和变量一起使用,可以在任何地方启动和调度。
  2. Goroutine比操作系统线程更轻量,可以被更高效地创建和销毁。
  3. Goroutine可以在同一个线程中并发执行,无需使用昂贵的线程切换开销。
  4. Goroutine之间可以通过通道(channel)进行通信和同步。
  5. Goroutine可以在任何时候被调度程序调度执行,因此它们的执行顺序是不确定的。
  6. 主goroutine退出后,其它的工作goroutine也会自动退出

channel

channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。

channel是一个数据类型,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

引⽤类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

    make(chan Type)  //等价于make(chan Type, 0)
    make(chan Type, capacity)

当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。

语法格式:

    channel <- value      //发送value到channel
    <-channel             //接收并将其丢弃
    x := <-channel        //从channel中接收数据,并赋值给x
    x, ok := <-channel    //功能同上,同时检查通道是否已关闭或者是否为空

无缓冲

要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

有缓冲

并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。

只有通道中没有要接收的值时,接收动作才会阻塞。

只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

借助函数 len(ch) 求取缓冲区中剩余元素个数, cap(ch) 求取缓冲区元素容量大小。

注意:

  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
  • 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭channel后,可以继续从channel接收数据;
  • 对于nil channel,无论收发都会被阻塞。
  • 可以使用 range 来迭代不断操作channel

有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

Select

Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。

select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:

    select {
    case <- chan1:
        // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
        // 如果成功向chan2写入数据,则进行该case处理语句
    default:
        // 如果上面都没有成功,则进入default处理流程
    }

在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。

如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

l 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。

l 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。

示例代码:

GO Modules

Go modules 是 Go 语言的依赖解决方案,目的是淘汰现有的 GOPATH 的使用模式。

GOPATH

GOPATH目录下一共包含了三个子目录,分别是:

  • bin:存储所编译生成的二进制文件。
  • pkg:存储预编译的目标文件,以加快程序的后续编译速度。
  • src:存储所有.go文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以$GOPATH/src/github.com/foo/bar的路径进行存放。

因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。

弊端:

  • A. 无版本控制概念. 在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。
  • B.无法同步一致第三方版本号. 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。
  • C.无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar

go mod命令

命令作用
go mod init生成 go.mod 文件
go mod download下载 go.mod 文件中指明的所有依赖
go mod tidy整理现有的依赖
go mod graph查看现有的依赖结构
go mod edit编辑 go.mod 文件
go mod vendor导出项目所有的依赖到vendor目录
go mod verify校验一个模块是否被篡改过
go mod why查看为什么需要依赖某模块

go mod 配置

Go语言提供了 GO111MODULE这个环境变量来作为 Go modules 的开关,其允许设置以下参数:

  • auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
  • on:启用 Go modules,推荐设置,将会是未来版本中的默认值。
  • off:禁用 Go modules,不推荐设置。

GOPROXY——这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。

GOPROXY 的默认值是:https://proxy.golang.org,direct

proxy.golang.org国内访问不了,需要设置国内的代理.

“direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取