阅读本文需要一些编程基础
环境配置就不说了
概述
可以先看看golang的FAQS,有很多问题都有设计者的解答,最为客观
https://go.dev/doc/faq#no_pointer_arithmetic
语言结构
- 先看一个简单的程序
package main
import "fmt"
func f() int {
fmt.Println("function f")
return 123456
}
var T = f()
func init() {
fmt.Println("init")
}
func main() {
var a int
sf, err := fmt.Scanf("%d", &a)
if err != nil {
return
}
println(123, 456)
fmt.Println(sf, 1)
fmt.Printf("%c\n", 48)
s := fmt.Sprintf("%s%d%T", "123", 123, 1234)
fmt.Println(s)
}
- 首先观察这个程序,
package
指明当前的包名,import
表示引入了哪些包,这里的fmt
包是一个golang的标准输入和输出包,这和Java类似,但是区别是main
函数的package
需要是main
- golang提供了两个基本的输入和输出,
print
和println
,它们在builtin
这个库里面,这个库包含许多其他的预定义标识符,包括int
等,这些标识符的实现个人理解是在编译器中进行的,包括保留字,编译器识别到这些预定义标识符之后,会把它们转化为特定的汇编代码,从而执行相关任务,所以你在golang的函数库里面是找不到print
函数的实现的 - 那么为什么要引入一个
fmt
库呢?因为golang
内置的输出函数没什么功能,而fmt
库里面提供了包括格式化输入输出,格式化字符串(fmt.Sprintf
)等等,这些函数的使用是和C++的输入和输出是类似的 - 上面展示了简单的输入和输出
- 这里面还有一些特殊的地方,就是这个main的开始不是真开始,结束也不是真结束,这是我自己个人理解,关于这句话的详细解释需要深入研究。那么放到这个文件中就是首先进行变量
T
的初始化,先执行f
函数,然后执行init
函数,之后才轮到main
的执行
数据类型
- 整型包括int,int8,int16,uint,uint8,uintptr等等
- 浮点数包括float32和float64
- 复数包括complex64
(实部和虚部各32位)
和complex128 - 还有一些其他类型包括
type
和rune
等等,使用它们的原因是便于区分字节值和字符类型的值,(相当于springboot中的@Service
和@Component
注解,作用基本没区别,只是用于分层),type
等价于uint8
,rune
等价于int32
简单说一点,每个字符占用的字节长度是不一定一样的,普通字符(ACSII字符)占用1个字节,但是汉字占用3个字节,所以如果使用字符串下标去修改字符串值可能是错误的
- 可以使用
len
函数来进行简单测试
func main() {
s := "你好"
fmt.Println(len(s))// 6
s2 := "s2"
fmt.Println(len(s2))// 2
}
- 正确的做法应该是先把字符串转化为
rune
,也就是每个字符都用4个字节来表示,然后再去修改,这样才是正确的
func main() {
s := "1s你好"
s2 := []rune(s)
fmt.Println(len(s))
}
- 此外,
string
类型和Java中是类似的,都是final
类,也就是不可修改,线程安全,所以不能够直接去修改,需要拷贝一份,再拷贝出来的数组上进行修改
语法
变量和常量
- 定义一个变量可以使用
var identifier type
这种方式,也可以使用var indentifier = value
这种形式,还可以使用短变量声明,也就是indentifier := value
,第一种方式适合延迟赋值,后两种方式都会推断变量类型 - 注意短变量声明相当于定义之后赋值,所以不能在已经定义了的变量之后再次使用,当然还有一个需要注意的是变量作用域,如果在一个代码段内进行短变量声明不会影响到代码段外面的相同名字的变量,也就是幽灵变量的问题
- 对于常量来说,适当使用
iota
可以起到枚举类型的作用,下面观察一下true
和false
的实现
const (
true = 0 == 0 // Untyped bool.
false = 0 != 0 // Untyped bool.
)
// 下面的例子就是iota
const (
MONDAY = iota // 0
TUESDAY// 1
WEDNESDAY// 2
)
- 同时golang支持多变量赋值,比如C++中的
swap
函数我们可以一行实现
func main() {
x := 1
y := 2
x, y = y, x
println(x, y)
}
- 同样的,由于有指针的概念,golang中也有值和引用的区别,对于基本类型,关于函数参数,如果不取地址传进去的就是值,对于map等类型,传进去的是引用,更改之后会影响到原来的map
- 所以对于比较大的类型,我们应该传的是变量的引用,这样能加快速度
- 但是事情不是这么简单的,因为函数中定义的指针一般是存储在栈中的,但是如果指针作为函数返回值,会发生逃逸现象,也就是变量从栈逃逸到堆,进而引发后续的垃圾回收,导致性能降低;在golang语言中,由编译器进行逃逸分析之后选择到底把变量分配到堆区还是栈区而不是是否进行了
new
操作,所以说到底传值还是引用需要综合考虑,如果涉及到大量的修改操作传引用比较好,否则传值可以减少垃圾回收的次数
运算符
- golang的运算符和C++类似,就不细说了
语句
- 只讲一些比较特殊的,
golang
没有while
,只有for
。然后有一个特殊的select
语句,
package main
func main() {
channel1 := make(chan int)
channel2 := make(chan int)
go func() {
for i := 0; i <= 100; i++ {
channel1 <- -10
}
}()
go func() {
for i := 0; i <= 10; i++ {
channel2 <- 2
}
}()
for i := 0; i <= 120; i++ {
select {
case <-channel1:
println(1)
break
case <-channel2:
println(2)
break
default:
println(3)
}
}
close(channel1)
close(channel2)
}
- select语句的每一个case都必须是一个通道
- 所有的channel表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果有某个通道可以执行,它就执行,否则被忽略
- 如果有多个case都可以运行,select会随机公平的选出一个执行,其他不会执行,否则如果有default则执行,要么就阻塞直到某个通道可以运行
结构体
- 下面的程序定义了一个结构体
package main
import (
"fmt"
"unsafe"
)
type st struct {
ck bool
id int32
c string
}
func main() {
p := &st{
ck: false,
id: 2147483647,
c: "123",
}
fmt.Println(unsafe.Sizeof(p))
}
- 有两种方式定义结构体变量,指针或者结构体本身,一般用前者,因为通常比较省空间,在定义结构体的时候,需要注意结构体内存对齐的问题,不同的定义顺序会导致结构体占用的空间不同,具体可以查阅相关资料
切片
- 切片是对数组的抽象,数组长度是不可变的,但是切片长度可变,有长度和容量的两个概念,当添加元素超过容量会引发扩容,扩容方式见源码文件
runtime\slice.go
下的growslice
函数,比较复杂
Map集合
- 这个map是无序的,如果想删除其中元素,需要调用
delete
方法
接口
- 我们声明一个接口并实现如下
package main
import "fmt"
type Animal interface {
Speak()
}
type Dog struct {
}
type Cat struct {
}
func (dog Dog) Speak() {
fmt.Println("Dog is speaking.")
}
func (cat Cat) Speak() {
fmt.Println("Cat is speaking.")
}
func main() {
animals := []Animal{Dog{}, Cat{}}
for _, value := range animals {
value.Speak()
}
}
- 在
golang
中没有类似Java中的implements关键字,实现一个接口只需要指明是哪个类实现的即可
异常
- 抛出一个异常通常使用
panic
,但是Go
提供了一种从异常中恢复的方法,就是recover
,看下面的例子
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("an error recovered")
}
}()
defer func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("here.")
}
}()
defer func() {
println(1)
panic("error 1.")
}()
println(2)
panic("error 2.")
}()
println(3)
panic("error 3.")
}
/*
3
2
1
here.
an error recovered
*/
- 如果想让程序从异常中恢复,需要在
panic
之后调用recover
,但是panic
程序就已经退出了,怎么调用recover
呢?所以需要利用Go
的特性defer
,也就是在panic
之后执行recover
函数,这样程序就会不会直接崩溃而是恢复正常
并发
- golang主要是通过goruntine来实现的并发,它的并发是协程并发,在语言层面支持协程,这可能是通过它的context来实现的,因为需要记录上下文等信息
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for i := range ch {
fmt.Println(i)
}
}
- 上面就实现了一个简单的并发,我设置通道缓冲区大小是0,程序在
main goroutine
和go func
两个协程并发执行 - 此外
golang
还提供了sync
包来提供一些锁操作
交叉编译
- go可以在Windows下编译出能够在其他操作系统中运行的程序,主要有
GOOS
,GOARCH
,CGO_ENABLED
这三个环境变量 GOOS
表示目标平台的操作系统(linux,darwin,windows等)GOARCH
表示目标平台的体系架构,例如amd64等CGO_ENABLED
设为0表示禁用CGO
例如我们在Windows下编译生成Linux环境下的程序,可以用
go env -w CGO_ENABLED=0
go env -w GOOS=linux
go env -w GOARCH=amd64
go build -o hello main.go
- 注意需要看自己的Linux系统是什么内核架构,如果是
x84-64
,需要用
go env -w CGO_ENABLED=0
go env -w GOOS=linux
go env -w GOARCH=x86
go build -o hello main.go
这里的-o
指的是将生成的文件命名为hello
,如果文件不能执行,需要给执行权限,也就是chmod u+x
依赖引入
- 在项目中引入的依赖如果想要更新,可以直接修改
go.mod
文件,也可以使用go get
命令,在这之后需要使用go clean -modcache
清除本地缓存的依赖项,否则还是原来的依赖
原因
如果你想把一个间接引入的包从v1升级到v2,可能会遇到这样的问题,你不知道哪个包依赖了这个包从而无法升级,可以使用下面的命令查看哪些包依赖了这个包,从而把源包升级之后才能将间接引入的包升级
go mod why -m 包名