likes
comments
collection
share

前端学go容易被坑的点(1)

作者站长头像
站长
· 阅读数 22

你知道本地开发如何引用本地的go包吗?

首先,我把结论说了,很多人不知道go在本地是如何引用外部包的。因为很多资料都停留在老版本,不是go mod的版本,让我也迷失了几天,发现网上很多说的都不对。

以下是正确版本:

1、在当前项目目录查找module(也就是go.mod文件项目里对应的package)。找到module后,用import module_name/包的目录路径引用

2、未找到,则逐级查找上级目录的go.mod(是不是有点像我们npm的node_modules的递归向上查找)

3、所有需要导入的路径中如果在 go.mod 中指定了版本,则从 $GOPATH/pkg/mod/ 下取得相应版本进行导入(我们下载的第三方包也是放在这个文件夹下的)

4、如果没有被指定则从 GOPATH/src/ 或 GOROOT/src/ 中进行查找导入。

举一个例子

一、引入的包在同一项目下

在实际开发中,这是我们最见的场景。

一般情况下我们在项目的目录下面,会建很多的包,他们并不冲突,比如下面结构图中的:kun-package

.
├── main.go
├── go.mod
└── kun-package
    └── Hello.go

kun-package/ Hello.go

package kun-package

import "fmt"

func New(){
    fmt.Println("kun-package.New")
}

go.mod 文件:

module moduledemo

go 1.17

然后在moduledemo/main.go中按如下方式导入kun-package

package main

import (
    "fmt"
    "moduledemo/kun-package"
)
func main() {
    mypackage.New()
    fmt.Println("main")
}

二、不在同一项目下面

其实难免我们要引用的包,是别人项目下面的,而不是自己项目下面的。

我们先来看一个项目结构图:

project01
├── go.mod
└── main.go
project02
└── kun-package
    ├── go.mod
    └── hello.go

假设project01要引用project02的包

project02/kun-package/go.mod

module kun-package

go 1.17

project01/go.mod

module project01

go 1.17

require kun-package v0.0.0

replace kun-package => ../project02/kun-package

注意了 replace是关键,他把我们引用的路径变为了相对路径,然后就可以在01项目的main文件中愉快的使用了:

package main

import (
 kunPackage "kun-package"
)

func main() {
 kunPackage.SayHello()
}

字符串类型的坑(byte和rune)

请看:

// 准备一个字符串类型
var house = "中国人"
fmt.Println(len(house))

结果是9,len是取的字节数,又因为go解析字符串是以utf8为基础,中这个字在utf8中是3个字节展示的,所以一共9个字节。

所以我们可以看到,go的字符串其实是byte类型。rune是按字符算的,所以我们需要把它转为字符,算出来的字符数才是我们想要的

var house = "中国人"
b := []rune(house)
fmt.Println(len(b))

这样就返回3了。

同时还需要注意,for循环中,是按字节取数据的,for range是按字符的,如下:

var s = "中国人"
fmt.Printf("the length of s = %d\n", len(s)) // 9
for i := 0; i < len(s); i++ {
  fmt.Printf("0x%x ", s[i]) // 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
}
fmt.Printf("\n")
var s = "中国人"
fmt.Println("the character count in s is", utf8.RuneCountInString(s)) // 3
for _, c := range s {
  fmt.Printf("0x%x ", c) // 0x4e2d 0x56fd 0x4eba
}
fmt.Printf("\n")

认识指针地址和指针类型

一个指针变量可以指向任何一个值的内存地址,这个跟32位操作系统和64位操作系统有关, 分别占用 4 或 8 个字节。

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。

每个变量在运行时都拥有一个地址,也就是在内存的位置,如下 & 就是取地址操作符

ptr := &v    // v 的类型为 T

其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。

除了取地址操作符,我们还有一个从地址里取值的操作符 *,请看下面案例

    // 准备一个字符串类型
     var a = "a"
 
     // 对字符串取地址, ptr类型为*string, 这里ptr是指针
     ptr := &a 
 
     // 这里把指针里存的值取出来,赋值给了value,其实value就是"a" 字符串
     value := *ptr

go里面的错误处理跟js的思路不一样

go希望你对所有可能出错的地方,自己在函数返回的时候,多返回一个error,然后让开发人员自己养成防御错误产生的习惯,所以你在写函数的时候,一定要注意可能发生的各种错误。

所以我在这里简单总结一下,go处理错误的核心思路:

Go语言中的错误处理主要通过在函数返回值中返回一个特殊的错误类型(通常是 error 接口)来实现。

核心思路如下:

  1. 函数的返回值中经常包含一个 error 类型的值,这个值可以是 nil,也可以是非 nil 的。如果是 nil,表示函数执行成功;如果是非 nil 的,则表示函数发生了错误。
  2. 在调用函数时,应该判断函数的返回值中的错误是否为 nil,如果是非 nil 的,则表示发生了错误,需要进行相应的处理。
  3. Go语言中也提供了 defer 关键字,可以在函数返回前调用一个函数,用于实现资源释放等清理工作。

通过这种方式,Go语言实现了错误处理的核心思路,可以帮助开发人员更方便地处理错误,并且代码风格简洁易懂。

go里的切片增删改跟js数组大不一样

在js里,对数组的操作,可以有很多,比如push,pop,shift,unshift,splice等等,可是在go里面,比如我们添加元素给切片,要使用append

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

所以我们就需要封装一个函数,往切片第i个位置添加元素,大概思路如下(你要封装的更好,需要判断下标是否越界,越界会让go程序报错)

var a []int{1,2,3}
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x

删除中间i个位置,go简单封装一个方法是这样的

var a []int{1,2,3}
a = append(a[:i], a[i+N:]...)

总感觉有点别扭。。。

类型断言和类型选择

类型断言

在Go语言中类型断言的语法如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。

也就是说变量或者常量的x是否是T类型。

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。

类型选择

类型选择 是一种按顺序从几个类型断言中选择分支的结构。

类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

switch v := i.(type) {
case T:
    // v 的类型为 T
case S:
    // v 的类型为 S
default:
    // 没有匹配,v 与 i 的类型相同
}

举例如下:

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

返回

Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!

接口和类型的关系是什么

接□本质上是-种类型,确切地说,是指针类型。接□可以实现多态功能。如果—个类型实现 了某个接□‖则所有使用这个接□的地万都支持这种类型的值。

如果接□没有任何方法声明’则它就是-个空接□(|∩te付ace{}),类似ts的any类型。

接口变量默认值是nil,如果实现接口的类型支持相等运算, 则可做相等运算否则会报错.

var a interface{}
fmt.Println(a == nil)
fmt.Println(a == 2)

返回

true
false

Go语言的接口不支持宣接实例化,但支持赋值操作,从而快速实现接口与实现类的映射。

将实现接口的对象实例赋值给接口

type Num int

func (x Num) Equal(i Num) bool {
	return x == i
}

func (x Num) LessThan(i Num) bool {
	return x < i
}

func (x Num) MoreThan(i Num) bool {
	return x > i
}

func (x *Num) Multiple(i Num) {
	*x = *x + 1
}

func (x *Num) Divide(i Num) {
	*x = *x / i
}

相应的,我们定义一个接口:

type NumI interface {
	Equal(i Num) bool
	LessThan(i Num) bool
	MoreThan(i Num) bool
	Multiple(i Num)
	Divide(i Num)
}

接着,我们赋值

var x Num = 8
var y NumI = &x

这里为什么不能用

var y NumI = x

因为接口类型本质是指针啊,所以不能这么做,但是明明Num里 Num指针,也就是Num,并没有实现比如Equal方法啊,因为Equal方法的接受体是Num不是Num,这个咋解释呢?

因为Go语言会根据下面这样的非指针成员方法:

func (x Num) Equal(i Num) bool {
	return x == i
}

自动生成一个新的与之对应的指针成员方法:

func (x *Num) Equal(i Num) bool {
	return (*x).Equal(i)
}

接口赋值

接口赋值并不要求两个接口完全等价(方法完全泪同),如果接口A的方法列表是接口B的方法列表的子集,则接口B可以赋值给接口A。

下篇接着讲并发编程,反射等常见坑点

参考资料:

  • 《go web编程》
  • 《go语言小书》
  • 官网
转载自:https://juejin.cn/post/7195832034244739130
评论
请登录