【后端开发】Golang基础语法中可能会踩的坑1(go mod, 2006, panic, range, slice…)
文章目录
从其他语言转到Go时容易碰到的一些踩坑。
参考资料: 1, 2, 3, 4, 5, 6
1、go mod: disallowed version string
问题:通过go get拉取公司私有包的时候报错:invalid: disallowed version string,但master、develop分支则可以正常拉取。
解决:
- 问题的关键在分支名称中的’/',在gomod中斜杠是有其他的用意的,没有设置GOPRIVATE时,默认拉取依赖都会走goproxy,所以会报错。
- 设置GOPRIVATE后,gomod就不会走goproxy而直接在源地址拉取,就不会报错了。
go env -w GOPRIVATE=gitlab.xxxx.com
go get -u gitlab.xxxx.com/xxxx@feature/xxxx
go: downloading gitlab.xxxx.com/xxxx v1.0.4
2024.8 补充一个版本:
问题:
- 设置了上面的GOPRIVATE仍然不行
- 参考go官方issue,stack over
go get github.com/xxx/api@feature/test1
go get: github.com/xxx/api@feature/test1: invalid version: version "github.com/xxx/api@feature/test1" invalid: disallowed version string
解决方案:
- 考虑搜索“分支名带斜杠拉不下来”,git地址后面多出一个斜杠“/“问题的解决, 方案1, git的时候带上账户
- 考虑搜索“go get如何拉取指定的分支”,参考1, 2, 3 得到解决方案,拉tag即可,事实上这个方案在官方的issue回复里也有提到。
go get golang.org/x/text@latest
go get golang.org/x/text@master # 分支带了/拉不下来
go get golang.org/x/text@v0.3.2
go get golang.org/x/text@342b2e # 替代解决方案!
2、Format(“2006-01-02 15:04:05”)
问题:
- Go 语言的时间格式化模板,不像其他很多语言,使用 “yyyy-mm-dd” 类似的格式。
- 而是采用其特殊的模板参数,然后实际的时间,会按照这个格式进行格式化。
解决:
- 没错,就是下面这个时间,换成任何其他时间都不行,指定格式的时候只能用下面这个时间
//时间
"Mon Jan 2 15:04:05 MST 2006"
"2006-01-02 03:04:05PM -0700"
// 格式化方式
newTime := time.Unix(x, 0).Format("2006-01-02 15:04:05")
Go 语言 time 库中,预定义的时间戳模板参数常量也可以使用
const (
ANSIC = "Mon Jan _2 15:04:05 2006"
UnixDate = "Mon Jan _2 15:04:05 MST 2006"
RubyDate = "Mon Jan 02 15:04:05 -0700 2006"
RFC822 = "02 Jan 06 15:04 MST"
RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 (小时数差标识时区)
RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 (小时数差标识时区)
RFC3339 = "2006-01-02T15:04:05Z07:00"
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
Kitchen = "3:04PM"
// Handy time stamps.
Stamp = "Jan _2 15:04:05"
StampMilli = "Jan _2 15:04:05.000"
StampMicro = "Jan _2 15:04:05.000000"
StampNano = "Jan _2 15:04:05.000000000"
)
3、使用 defer 和 recover 捕获 panic
panic是什么?
- Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等。这些运行时错误会引起painc异常。
- 一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数,逐层展开调用栈,运行在每一层调用中定义的
defer
语句。 - 如果 panic 未被恢复(即未被 recover 捕获) 最终会导致程序崩溃,并输出错误信息和调用栈。
- 程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。
一些panic的例子:
- 运行时错误:数组越界:
var arr [3]int \\ print(arr[4])
- 手动触发:
panic("Something went wrong!")
使用 defer 和 recover 捕获 panic
package main
import "fmt"
func main() {
// 捕获 panic,避免程序崩溃
defer func() {
if err := recover(); err != nil {
fmt.Println("Recovered from panic:", err)
}
}()
fmt.Println("Starting the program")
// 触发 panic
panic("Something went wrong!")
fmt.Println("This line will not be executed")
}
引用:1
补充一些会导致panic的场景
直接使用值为 nil 的 slice、map
允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic
// map 错误示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正确声明,分配了实际的内存
}
// slice 正确示例
func main() {
var s []int
s = append(s, 1)
}
将 JSON 中的数字解码为 interface 类型
在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
fmt.Printf("%T\n", result["status"]) // float64
var status = result["status"].(int) // 类型断言错误
fmt.Println("Status value: ", status)
}
// 将 decode 的值转为 int 使用
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
var status = uint64(result["status"].(float64))
fmt.Println("Status value: ", status)
}
4、range引用, if作用域, switch break
在 range 迭代 slice、array、map 时通过更新引用来更新元素
- 在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:
func main() {
data := []int{1, 2, 3}
for _, v := range data {
v *= 10 // data 中原有元素是不会被修改的
}
fmt.Println("data: ", data) // data: [1 2 3]
}
func main() {
data := []int{1, 2, 3}
for i, v := range data {
data[i] = v * 10
}
fmt.Println("data: ", data) // data: [10 20 30]
}
if变量作用域
- 在 Go 的 if 语句中,变量可以在条件语句中定义,这些变量的作用域只在 if 语句内。
if x := someFunc(); x > 0 {
fmt.Println("x is positive")
}
// fmt.Println(x) // 错误:x 未定义
x := someFunc()
if x > 0 {
fmt.Println("x is positive")
}
fmt.Println(x)
switch 语句 无需显式 break
- 在 Go 的 switch 语句中,每个 case 块自动带有 break,无需显式添加。
- 但是没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:
// break 配合 label 跳出指定代码块
func main() {
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
//break // 死循环,一直打印 breaking out...
break loop
}
}
fmt.Println("out...")
}
5、array, map, slice,切片合并,三个点…
将slice打散展开,还有种情况就是通过append合并两个slice:
stooges := []string{"Moe", "Larry", "Curly"}
lang := []string{"php", "golang", "java"}
stooges = append(stooges, lang...) // 三个点
fmt.Println(stooges)
stooges := [...]string{"Moe", "Larry", "Curly"} //等价于stooges := [3]string{"Moe", "Larry", "Curly"}
arr := [...]int{1, 2, 3}
切片引用与数据共享
- 切片本质上是对底层数组的引用,多个切片引用同一底层数组时,修改其中一个切片会影响到其他引用该数组的切片。
a := []int{1, 2, 3, 4}
b := a[1:3]
b[0] = 99
此时 `a` 变成了 `[1, 99, 3, 4]`。
如果需要独立的副本,请使用 `copy` 函数创建一个新的切片。
a := []int{1, 2, 3, 4}
b := make([]int, len(a[1:3]))
copy(b, a[1:3])
b[0] = 99
此时 `a` 仍然是 `[1, 2, 3, 4]`,`b` 是 `[99, 3]`。
数组和切片的不同。数组是固定长度的,而切片是动态长度的。
- 超过切片容量的扩展
- 在向切片中追加元素时,如果超过了原有容量,Go 会创建一个更大的底层数组,旧切片与新切片之间没有数据共享。
var a [4]int // 数组,长度固定为 4
s := []int{1, 2, 3, 4} // 切片
a := make([]int, 2, 2)
a = append(a, 1)
fmt.Println(cap(a)) // 输出 4,因为容量被扩展了
未初始化 map 就直接使用,会导致运行时错误。
# Python
d = {}
d['key'] = 'value'
print(d) # 输出 {'key': 'value'}
var m map[string]string
m["key"] = "value" // 导致 panic: assignment to entry in nil map
//解决方案: 使用 make 函数初始化 map。
m := make(map[string]string)
m["key"] = "value"
fmt.Println(m) // 输出 map[key:value]
访问不存在的键不会抛出错误,但会返回零值。
# python
d = {'key': 'value'}
print(d['non_existent']) # 抛出 KeyError
m := map[string]string{"key": "value"}
fmt.Println(m["non_existent"]) // 输出空字符串 ""
//解决方案: 使用第二个返回值来检查键是否存在。
value, exists := m["non_existent"]
if !exists {
fmt.Println("key does not exist")
} else {
fmt.Println(value)
}