go 基础中的一些坑
类型转换
在 go
语言中,类型转换是显式的,不会自动转换
func main(){
i := 100
var f float64
f = float64(i)
}
string
转换成 int
需要借助 strconv
包
使用 strconv.Atoi
函数将 string
转换成 int
,转换后它会输出两个值:
- 一个是转换后的值
- 一个
error
,如果转换出错了,第一个值是0
func main() {
str := "100s"
i, err := strconv.Atoi(str)
if err != nil {
fmt.Println(err)
}
fmt.Println(i)
}
这个包还提供了其他的类型的字符串转换方式
strconv.ParseBool
,将字符串bool
转换成bool
类型strconv.ParseFloat
,将字符串float
转换成float
类型strconv.ParseInt
,将字符串int
转换成int
类型strconv.ParseBool
,将bool
类型转成string
类型
数组
go
中数组是定长的,也就是说你需要指定数组的长度,未初始化的项默认值是 0
数组需要注意一个越界问题,也就是说不能访问数组的长度之外的元素
func main() {
// 10 个元素的数组
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(arr[9]) // 10
fmt.Println(arr[10]) // 会报错
arr2 := [10]int{1, 2, 3}, // 从下标 4 开始到下标 9 的元素都是 0
}
数组的长度是不可改变的
func main() {
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
arr = [10]int{1, 2, 3} // 数组的长度是不能改变的
}
slice
slice
是一种动态数组,它是由相同类型元素组成的序列
slice
底层由三部分组成:
- 指向底层数组的指针
slice
的长度slice
的容量
在底层数组没有被扩展之前,slice
的长度和容量相等,当我们向 slice
中添加元素时,如果超过了 slice
的容量,那么 go
就会重新分配一个更大的底层数组,并将原始数组复制到心的数组中,这个过程被称为扩容
初始化一个 slice
有两种方式:
- 直接使用字面量的方式初始化一个
slice
func main() {
s := []int{1, 2, 3, 4}
}
- 使用
make
函数创建一个slice
,它接收三个参数:
- 第一个参数是类型
- 第二个参数是长度
- 第三个参数是容量
- 第三个参数可以省略,也就是说如果不指定容量,那么容量等于长度
func main() {
s := make([]int, 3, 5) // 创建一个长度为 3,容量为 5 的切片
}
slice
的访问方式和数组一样:
- 通过下标访问
arr := [4]int{1, 2, 3, 4}
fmt.Println(arr[0]) // 1
s := []int{1, 2, 3, 4}
fmt.Println(s[0]) // 1
- 通过区间访问(前闭后开)
arr := [4]int{1, 2, 3, 4}
fmt.Println(arr[0:2]) // 1,2
s := []int{1, 2, 3, 4}
fmt.Println(s[0:2]) // 1,2
- 区间访问的时候,如果省略了开始的下标,那么默认是
0
- 如果省略了结束的下标,那么默认是
slice
的长度
- 不能越界访问
删除 slice
中的某项元素,可以使用 append
函数,三个点是展开操作符,它会将 s
中的元素展开
s := []int{1, 2, 3, 4, 5, 6, 7, 8}
s = append(s[:1], s[2:]...)
数组和 slice
最大的区别是,数组的长度是固定的,而 slice
的长度是可变的
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8} // 数组
s := []int{1, 2, 3, 4, 5, 6, 7, 8} // 切片
数组可以转变成切片
arr := [8]int{1, 2, 3, 4, 5, 6, 7, 8} // 数组
s := arr[:] // 将数组转换成切片
slice 是引用类型
如果一个函数接收的是切片,那么它接收的是切片的引用,也就是说它会改变原始切片的值
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(s) // [1 2 3 4 5 6 7 8]
addOne(s)
fmt.Println(s) // [2 3 4 5 6 7 8 9]
}
// 接收的事切片的引用
func addOne(n []int) {
for i := 0; i < len(n); i++ {
n[i] = n[i] + 1
}
}
如果函数接收的是数组,那么它接收的是数组的值,也就是说它不会改变原始数组的值
func main() {
s := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(s) // [1 2 3 4 5 6 7 8]
addOne(s)
fmt.Println(s) // [1 2 3 4 5 6 7 8]
}
func addOne(n [8]int) {
for i := 0; i < len(n); i++ {
n[i] = n[i] + 1
}
}
那么数组要实现引用传递怎么办呢?可以使用指针
func main() {
s := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(s) // [1 2 3 4 5 6 7 8]
addOne(&s)
fmt.Println(s) // [2 3 4 5 6 7 8 9]
}
func addOne(n *[8]int) {
for i := 0; i < len(n); i++ {
n[i] = n[i] + 1
}
}
最后,slice
如何传递指针?在函数内部需要使用 *
来解引用,然后在对 slice
进行操作
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Println(s)
addOne(&s)
fmt.Println(s)
}
func addOne(n *[]int) {
// 先对 n 解引用
_n := *n
for i := 0; i < len(_n); i++ {
_n[i] = _n[i] + 1
}
}
map
map
未初始化,可以取值,但赋值会报错,
func main() {
var m1 map[string]int
fmt.Println(m1["age"]) // 0
m1["age"] = 1 // 报错
}
map
的初始化有两种方式:
m1 := map[string]int{"age": 1} // 字面量的方式
m2 := map[string]int{} // 这种也是字面量
m3 := make(map[string]int) // 使用 make 函数
如何判断一个属性在不在 map
中呢?可以使用 ok
来判断
func main() {
m2 := make(map[string]int)
m2["age"] = 25
a, ok := m2["age"]
fmt.Println(a, ok) // 25 true
a, ok = m2["age2"]
fmt.Println(a, ok) // 0 false
}
switch
go
中的 switch
如果命中某条 case
语句后,就不会命中其他 case
语句了
switch
可以使用 x.(type)
的方式类判断一个变量的类型,x.(type)
只能在 switch
语句中使用,不能在 if
语句中使用
func typeof(x interface{}) {
switch x.(type) {
case int:
fmt.Println("int")
case string:
fmt.Println("string")
default:
fmt.Println("unknown")
}
}
for
在 for
循环中,如果操作指针的话,会有一个问题,如下所示:添加到 oddNumbers
中都是 7
因为 number
是一个变量,它的地址是不变的,所以 oddNumbers
中的元素都是 number
的地址,而 number
最后的值是 7
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7}
var oddNumbers []*int
for _, number := range numbers {
// number 的地址是不变的
oddNumbers = append(oddNumbers, &number)
}
for _, oddNumber := range oddNumbers {
fmt.Println(*oddNumber)
}
}
如何解决这个问题呢?可以使用一个临时变量,每一个循环进来的时候,都创建一个临时变量,然后将它的地址添加到 oddNumbers
中
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7}
var oddNumbers []*int
for _, number := range numbers {
// 每次循环都会创建一个新变量,然后将它的地址添加到 oddNumbers 中
tmp := number
oddNumbers = append(oddNumbers, &tmp)
}
for _, oddNumber := range oddNumbers {
fmt.Println(*oddNumber)
}
}
error
任何一个实现了 Error()
方法的类型都可以作为错误类型
type MyError struct {
message string
code int
}
func (e MyError) Error() string {
return e.message
}
func add() (*int, error) {
var myError = MyError{
message: "This is an error",
code: 500,
}
return nil, myError // 使用自定义的错误
}
判断一个 error
是什么类型最好使用 errors.Is
函数,不要用 ==
来判断
e1 := fmt.Errorf("error 1: %w", io.EOF)
fmt.Println(errors.Is(e1, io.EOF)) // true
fmt.Println(e1 == io.EOF) // false
error
类型断言可以使用 errors.As
函数,不要使用 err.(xx)
因为 errors.As
和 errors.Is
函数是可以判断包装过的 error
package
在一个 package
中执行顺序:
- 先执行
const
常量 - 再执行
var
变量 - 然后再执行
init
函数 - 最后执行
main
函数
如果有引入其他的 package
,那么它会先执行其他的 package
的 const
,var
,init
,然后再执行当前 package
的 const
,var
,init
,这是一个深度优先的顺序
go mod
是一个用于管理 go
的依赖模块,它会将依赖的模块下载到 go
的缓存目录中,然后在 go.mod
文件中记录下来,一般存储在 $GOPATH/pkg/mod
目录中
go mod
的命令:
go mod init
:初始化一个go.mod
文件go mod tidy
:根据go.mod
文件整理依赖go mod download
:下载go.mod
文件中的依赖,但不安装go mod verify
:验证依赖是否正确和完整go mod graph
:以图形化显示模块之间的依赖关系go mod why
:解释模块为什么需要特定的依赖
receiver
指针类型 receiver
使用指向该类型的指针作为接收者;值类型 receiver
使用该类型的值作为接收者
定义一个 Animal
接口,它有一个 Eat
方法,定义 Dog
结构体,它有一个 Say
方法,然后实现 Animal
接口
使用指针类型作为 receiver
,实现接口需要使用指针类型的值
type Animal interface {
Eat()
}
type Dog struct {
Name string
}
func (d *Dog) Say() {
fmt.Printf("Name is %v", d.Name)
}
func main() {
// 这里只能使用指针类型的值
var a Animal = &Dog{Name: "uccs"}
a.Say()
}
使用值类型作为 receiver
,实现接口可以使用值类型的值,也可以使用指针类型的值
type Animal interface {
Say()
}
type Dog struct {
Name string
}
func (d Dog) Say() {
fmt.Printf("Name is %v", d.Name)
}
func main() {
// 这里可以使用指针类型的值
var a Animal = &Dog{Name: "uccs"}
a.Say()
// 也可以使用值类型的值
var a1 Animal = Dog{Name: "astak"}
a1.Say()
}
interface
接口类型断言:
func AnimalSleep(e Eater){
if s, ok := e.(Animal); ok {
s.Sleep()
}
}
抽象接口的实现:
type Worker interface {
doWork()
Start()
}
type BaseWorker struct {
Worker
}
func (b *BaseWorker) Start() {
fmt.Println("start")
b.doWork()
fmt.Println("end")
}
type NormalWorker struct {
BaseWorker
}
func (n *NormalWorker) doWork() {
fmt.Println("do work")
}
func NewNormalWorker() Worker {
n := &NormalWorker{BaseWorker{}}
// 这边需要赋值,不然会报错
n.Worker = n
return n
}
func main() {
NewNormalWorker().Start()
}
goroutine 中的竞态
当有多个 goroutine
对同一个变量进行读写操作时,就会出现竞态
因为写入一个变量的操作不是原子的,一般会分为三步:
- 读取变量的值:
read counter
- 对变量的值进行操作:
counter = counter + 1
- 将最新的值写入变量:
write counter
var counter int32
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
counter = counter + 1
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(counter) // 不一定是 1000
}
如何解决这个问题呢:
- 使用
atomic
包中的原子操作- 在
x86
架构中,atomic.AddInt32
函数使用lock xaddq
指令来实现原子加操作
这两种方法是一样的,只是var counter int32 func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func(i int) { // 这个操作是原子的 atomic.AddInt32(&counter, 1) wg.Done() }(i) } wg.Wait() fmt.Println(counter) }
atomic.AddInt32
的方法更加简洁var counter = atomic.Int32{} func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func(i int) { counter.Add(1) wg.Done() }(i) } wg.Wait() fmt.Println(counter.Load()) }
- 在
atomic.CompareAndSwapInt64
函数是使用CPU
的原子指令来实现的,它使用了CPU
提供的compare-and-swap
指令来保证原子性CAS
指令可以原子的比较并交换一个内存地址中的值,它有三个操作数:内存地址addr
,期望的旧值old
,新值new
- 如果内存地址
addr
中的值等于old
,那么将new
的值写入addr
中,否则不做任何操作
var counter int32 func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func(i int) { for { if swaped := atomic.CompareAndSwapInt32(&counter, counter, counter+1); swaped { break } } wg.Done() }(i) } wg.Wait() fmt.Println(counter) }
- 使用锁
var counter int32 var lock sync.Mutex func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func(i int) { lock.Lock() counter = counter + 1 lock.Unlock() wg.Done() }(i) } wg.Wait() fmt.Println(counter) }
转载自:https://juejin.cn/post/7349047049869246479