您现在的位置是:首页 >技术杂谈 >初学GoLang易错点网站首页技术杂谈
初学GoLang易错点
初学GoLang易错点
可能你是从其他编程语言转到Go语言,也可能是第一次接触的编程语言就是Go语言,都可能会遇到一些易错点。以下是整理的一些常见的go语言易错点,以及如何避免它们。
1. 不允许出现未使用的变量和import
Go语言要求你使用所有声明的变量和导入的包。如果你声明了一个变量或导入了一个包,但没有使用它,编译器会报错。为了避免这个问题,确保删除未使用的变量和导入。
2. 变量的定义语法与其他的语言不一样
在Go语言中,变量的定义语法与其他编程语言不同,需要注意变量类型在变量名之后如
,而不是在变量名之前。
你可以使用var
关键字声明一个变量,然后使用等号分配一个值。例如:
var x int = 10
var arr [5]int
var m map[string]int
你也可以使用短变量声明(:=
)来声明并初始化一个变量,这种语法可以自动推断变量类型,因此你不需要显式地指定变量类型。但是,短变量声明只能在函数内部使用。
x := 10
另外,Go语言还支持多个变量的声明和初始化。例如:
var x, y int = 10, 20
3. 多行的list和map语句
Go语言的多行语法可以使代码更加易于阅读和维护。需要注意的是,在多行语法中,每个值或键值对都在自己的行上,并且在最后一个值或键值对后面也要有一个逗号
。
var myList = []int{
1,
2,
3, // 注意这里的逗号
}
m := map[string]int{
"foo": 1,
"bar": 2,
"baz": 3,
}
4. 不需要的方法返回值可以通过下划线接收
如果你不需要一个函数的返回值,可以使用下划线(_
)来忽略它。例如:
func doSomething() (int, error) {
// ...
}
func main() {
_, err := doSomething()
if err != nil {
// handle error
}
}
5. 变量及函数可见性
在 Go 语言中,命名的大小写规则决定了它们的可见性。
首先,Go 语言中以大写字母开头的标识符表示公有(可以被外部代码访问)。例如:
func Add(a, b int) int {
return a + b
}
这里的 Add
函数以大写字母开头,表示它是一个公有函数。可以在该包外面的代码中调用该函数。
而以小写字母开头的标识符表示私有(只能被本包内的代码访问)。例如:
func add(a, b int) int {
return a + b
}
这里的 add
函数以小写字母开头,表示它是一个私有函数。只能在该包内的其他代码中调用该函数。
同样,变量、常量、类型、结构体和接口等也遵循这个规则,以大写字母开头表示公有,以小写字母开头表示私有。除此之外,Go 语言还有一些约定俗成的命名规范,如:
- 使用 camelCase 命名法:即所有单词的首字母小写,除了第一个单词的首字母大写。
- 对于布尔型变量,可以使用 is 或 has 开头的命名方式,如
isActive
、hasKey
等。 - 对于接收者类型为某种类型 T 的方法,建议将方法名命名为
T.Method
的形式,如time.Duration.Seconds()
。 - 在命名时要尽量避免使用缩写,除非是普遍通用的缩写,如 ID、HTTP 等。
这些命名规范有助于提高代码的可读性和可维护性,并且与 Go 语言的规范和习惯一致。
6. 类型转换
在Go语言中,类型转换需要显式地进行。
- 值类型转换
值类型转换是将一个值从一种类型转换为另一种类型。例如,将一个整数转换为浮点数。值类型转换的语法如下:
var x int = 10
var y float64 = float64(x)
在这个例子中,我们将整数变量x
转换为浮点数变量y
。需要注意的是,我们使用float64(x)
来执行显示转换。
在Go语言中,string类型与其他类型之间的转换
是非常常见的。以下是常见的string类型与其他类型之间的转换方法:
//将string类型转换为int类型:
var str = "10"
var x, _ = strconv.Atoi(str)
//`strconv.Atoi()`函数返回两个值,第一个是转换后的整数,第二个是错误信息。因此,我们使用`_`来忽略错误信息。
//将int类型转换为string类型:
var x int = 10
var str = strconv.Itoa(x)
//将string类型转换为float类型:
var str = "10.5"
var x, _ = strconv.ParseFloat(str, 64)
//将float类型转换为string类型:
var x float64 = 10.5
var str = strconv.FormatFloat(x, 'f', 2, 64)
在Go语言中,string、rune和byte数组之间有着密切的关系,同时也可以相互转换。
byte类型用于表示8位无符号整数,通常用于处理二进制数据或者ASCII字符;rune用于表示Unicode字符,包括ASCII字符和非ASCII字符。
//将string类型转换为byte数组,string类型可以通过转换为byte数组来获取其中的每一个字符的ASCII码值。
//需要注意的是,如果string类型中包含非ASCII字符,那么在转换为byte数组时可能会出现乱码或者丢失字符的情况
var str = "hello"
var bytes = []byte(str)
//将byte数组转换为string类型:
var bytes = []byte{'h', 'e', 'l', 'l', 'o'}
var str = string(bytes)
//将string类型转换为rune类型,string类型是由一系列的rune类型组成的,将string类型转换为rune类型来获取其中的每一个字符。
var str = "hello"
var runes = []rune(str)
//将rune类型转换为string类型:
var runes = []rune{'h', 'e', 'l', 'l', 'o'}
var str = string(runes)
//将rune类型转换为byte数组,rune类型可以通过转换为byte数组来获取其中每一个字符的ASCII码值
//如果rune类型中包含非ASCII字符,那么在转换为byte数组时可能会出现乱码或者丢失字符的情况
var r rune = 'a'
var bytes = []byte(string(r))
//将byte数组转换为rune类型:
var bytes = []byte{'a'}
var r = rune(bytes[0])
- 指针类型转换
指针类型转换是将一个指针从一种类型转换为另一种类型。例如,将一个指向整数的指针转换为指向浮点数的指针。指针类型转换的语法如下:
var x int = 10
var p *int = &x
var q *float64 = (*float64)(unsafe.Pointer(p))
在这个例子中,我们将指向整数的指针p
转换为指向浮点数的指针q
。需要注意的是,我们先使用unsafe.Pointer
将p
转换为void*
类型,然后将其转换为指向浮点数的指针类型。
- 类型别名
类型别名是将一个类型定义为另一个名称。例如,将int
类型定义为myint
类型。类型别名的语法如下:
type myint int
var x myint = 10
在这个例子中,我们将int
类型定义为myint
类型,并将整数变量x
初始化为10。需要注意的是,myint
类型与int
类型是相同的,只是名称不同。
7.函数与方法
在Go语言中,函数和方法都是使用func
关键字定义的。虽然它们看起来很相似,但是它们有一些关键的区别。
函数是独立的,不属于任何类型。函数定义的语法如下:
func add(a int, b int) int {
return a + b
}
result := add(1, 2)
fmt.Println(result) // 3
方法是与类型相关联的函数。方法有一个特殊的接收者参数,即该方法所属的类型
,例如:
type Circle struct {
x, y, radius float64
}
// 定义Circle类型的方法
func (c Circle) area() float64 {
return math.Pi * c.radius * c.radius
}
c := Circle{x: 0, y: 0, radius: 5}
area := c.area()
fmt.Println(area) // 78.53981633974483
在上面的代码中,我们为Circle类型定义了一个方法area(),它接收一个Circle类型的参数c,并返回一个浮点数。在方法的定义中,接收者参数(c Circle)出现在func关键字和方法名之间
。在方法体内部,可以使用接收者来访问Circle类型的成员变量。
注意,方法的接收者参数可以是值类型(例如上面的Circle),也可以是指针类型(例如 *Circle)。对于值类型的接收者,方法中修改接收者的属性不会改变原始变量的值;而对于指针类型的接收者,方法中修改接收者的属性会影响到原始变量的值。
8. 包的使用
当我们在代码中使用一个包时,我们需要使用import关键字来导入这个包,然后就可以使用这个包中的函数、变量和类型了。
Go语言的包管理工具是go mod,它可以帮助我们管理项目的依赖关系。使用go mod,我们可以在项目中添加、删除、更新依赖包,并且可以自动解决依赖关系。当我们使用go mod来管理依赖关系时,我们需要在项目的根目录下创建一个go.mod文件,这个文件会记录项目的依赖关系和版本信息。
在使用go mod管理依赖关系时,我们可以使用go get命令来添加新的依赖包,使用go mod tidy命令来移除不再使用的依赖包,使用go mod vendor命令来将依赖包复制到项目的vendor目录下,以便离线构建项目。
9. 字符串需要通过转义符来包含不属于utf8的数据
在Go语言中,字符串是由一系列Unicode字符组成的,每个Unicode字符占用1至4个字节。如果要将一个字节序列插入到字符串中,其中包含不属于UTF-8编码的数据,则需要使用转义符来表示这些字节。例如:
//
为换行符
s := "Hello, 世界!
"
10. 值类型与引用类型
在 Go 语言中,值类型(Value Types)和引用类型(Reference Types)是两种不同的数据类型。它们有不同的特点和使用方式。
值类型
值类型代表实际存储值的变量。当创建一个值类型的变量时,这个变量将会被分配在栈上,并且它有自己独立的内存地址。由于值类型的变量具有自己独立的内存空间,因此对该变量进行操作不会影响其他变量。Go 语言中的基本数据类型和结构体都是值类型。例如:
var x int = 10 // x 是 int 类型的变量,它是一个值类型。
var y int = x + 5 // x 的值不会受到影响。
s := [3]int{1, 2, 3} //给定长度,s是一个数组
引用类型
引用类型把值保存在堆(Heap)上,并提供了对这些值的间接引用。引用类型的变量存储的是一个指向堆上实际数据的内存地址,而不是数据本身。由于多个变量可能共享同一个底层数据(也就是它们指向同一个内存地址),因此修改其中一个变量的值可能会影响到其他变量。
Go 语言中的引用类型包括 slice、map、channel 和指针类型等。例如,在下面的代码中,s 和 s1 都是指向同一个 slice 的指针,所以如果我们改变 s 的值,s1 的值也会相应地改变:
s := []int{1, 2, 3}//未给定长度,s是一个切片
s1 := s // s1 和 s 切片共享同一个底层数组
s[0] = 4 // 修改 s 中的第一个元素
fmt.Println(s) // [4 2 3]
fmt.Println(s1) // [4 2 3],因为 s1 指向的数组被修改了
如果想要对引用类型做函数参数有深入了解,可以前往阅读go语言切片做函数参数传递+append()函数扩容
11. new和make的区别
new
和make
都用于创建变量,但它们有一些区别:
-
new
是一个内置的函数,在 Go 中用于创建并初始化值类型的指针,其分配的空间默认被初始化为零值。因此new 仅分配空间,而没有执行初始化操作,不能用于初始化引用类型(比如 map、slice 和 channel 等),否则会在访问时出现 nil 引用错误。此时应该使用 make 函数。p := new(int) fmt.Println(*p) // 输出0,表示*p指向的int类型的变量的初始值
-
make
在 Go 中只用用于创建引用类型的实例,如 slice、map 和 channel,用法类似于 new,但是它返回的是一个初始化后的引用对象s := make([]int, 0, 10)
12. for range遍历集合返回键值对
在 Go 语言中,for range
语句用于遍历数组、slice(切片)、字符串、map 等集合类型。虽然 for range
可以返回集合的值,但实际上它返回的是索引或键和对应的值,而不是单纯的值。
具体来说,for range
返回的是两个值:第一个是索引或键,第二个是对应的值。在遍历数组或 slice 时,第一个值是值的索引,而在遍历 map 时,则是键。例如:
str := []string{"apple", "banana", "cherry"}
// 遍历 slice
for i, s := range str {
fmt.Println(i, s)
}
m := map[string]int{"apple": 1, "banana": 2, "cherry": 3}
// 遍历 map
for k, v := range m {
fmt.Println(k, v)
}
在上述代码中,第一个 for range
循环遍历了一个字符串 slice,每次迭代会返回一个字符串元素的索引和对应的值,然后将它们打印出来。第二个循环则遍历了一个 map,并将键值分别打印出来。
需要注意的是,在使用 for range
进行遍历时,被遍历的变量必须是可迭代的类型(如 slice、map 等),否则会导致编译错误。此外,在遍历过程中,for range
返回的值是只读的,因此不能在循环中修改集合中的元素。如果需要进行修改操作,可以使用下标或指向当前元素的指针来进行访问和修改。
13. print输出格式
在 Go 语言中,print 输出格式可以通过 fmt 包提供的一系列函数来控制。以下是其中一些常用的函数及其使用方式:
-
Print:根据默认格式将值输出到标准输出。
fmt.Print("Hello, ", "world!")
-
Printf:根据指定格式将值输出到标准输出,%d表示整数输出,将变量 age 的值作为 %d 的参数,这样就可以将其输出到字符串中。
age := 28 fmt.Printf("I am %d years old. ", age)
-
Sprintf:根据指定格式将值输出到字符串中。
msg := fmt.Sprintf("Hello, %s!", "Gopher")
-
Println:根据默认格式将值输出到标准输出,输出后进行换行。
fmt.Println("Hello,", "world!")
除了这些基本的输出函数外,还可以使用一些格式化动词来自定义输出格式。以下是一些常用的格式化动词和它们的含义:
- %v:默认格式,使用默认的字符串表示。
- %t:bool 类型,true 或 false。
- %d、%o、%x:整数类型,分别以十进制、八进制、十六进制输出。
- %f、%g、%e:浮点数类型,分别以普通、指数、科学计数法表示输出。
- %s:字符串类型,直接输出字符串。
- %q:字符串类型,带双引号并对需要转义的字符进行转义。
- %p:指针类型,以十六进制表示输出。
根据需要,可以在格式化动词中添加标志来指定输出的宽度、精度、对齐方式等。例如,%-10s 表示左对齐并且宽度为 10 的字符串类型。
14. 多线程的问题
Go语言是一种支持并发编程的现代编程语言。它通过goroutine和通道(channel)来实现多线程操作。确保正确地同步并发操作,以避免竞争条件和死锁
- Goroutine:Goroutine是Go语言中的轻量级线程。它们在Go运行时的调度器中运行,而不是直接在操作系统线程上运行。
创建一个goroutine非常简单,只需在函数调用前加上关键字go
。Goroutine的调度和管理由Go运行时自动处理,这使得并发编程变得更加简单。
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(1 * time.Second)
}
}
func main() {
go printNumbers() // 启动一个goroutine
time.Sleep(6 * time.Second)
}
- 通道(Channel):通道是Go语言中用于在goroutine之间传递数据的同步原语。
通道可以确保数据在不同的goroutine之间安全地传输,从而避免竞争条件
。通道的创建使用make
关键字,可以指定通道的类型和缓冲区大小。
package main
import (
"fmt"
)
func sendData(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i // 将数据发送到通道,在发送数据时使用 <- 运算符,接收数据则使用 <- 运算符:
}
close(ch) // 关闭通道
}
func main() {
ch := make(chan int) // 创建一个整数类型的通道
go sendData(ch) // 启动一个goroutine发送数据
for v := range ch { // 从通道接收数据
fmt.Println(v)
}
}
为了避免竞争条件和死锁,可以采用以下策略:
-
使用
通道进行同步
:通过在goroutine之间传递数据,而不是共享内存,可以避免竞争条件。通道可以确保数据在不同的goroutine之间安全地传输。 -
使用
互斥锁(Mutex)
:当需要共享内存时,可以使用互斥锁来确保同一时间只有一个goroutine访问共享资源。Go标准库中的sync.Mutex提供了这个功能。 -
使用
sync.WaitGroup
:sync.WaitGroup可以用来等待一组goroutine完成。它可以确保所有的goroutine都完成后,主程序才继续执行,从而避免死锁。 -
使用
select
语句:select语句可以用来同时处理多个通道操作。它可以防止死锁,因为它会在多个通道中选择一个可用的操作来执行。
通过使用这些策略,Go语言可以帮助您更轻松地编写并发程序,同时避免竞争条件和死锁。