您现在的位置是:首页 >技术杂谈 >初学GoLang易错点网站首页技术杂谈

初学GoLang易错点

Pistachiout 2024-06-12 12:01:02
简介初学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 开头的命名方式,如 isActivehasKey 等。
  • 对于接收者类型为某种类型 T 的方法,建议将方法名命名为 T.Method 的形式,如 time.Duration.Seconds()
  • 在命名时要尽量避免使用缩写,除非是普遍通用的缩写,如 ID、HTTP 等。

这些命名规范有助于提高代码的可读性和可维护性,并且与 Go 语言的规范和习惯一致。

6. 类型转换

在Go语言中,类型转换需要显式地进行。

  1. 值类型转换

值类型转换是将一个值从一种类型转换为另一种类型。例如,将一个整数转换为浮点数。值类型转换的语法如下:

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])
  1. 指针类型转换

指针类型转换是将一个指针从一种类型转换为另一种类型。例如,将一个指向整数的指针转换为指向浮点数的指针。指针类型转换的语法如下:

var x int = 10
var p *int = &x
var q *float64 = (*float64)(unsafe.Pointer(p))

在这个例子中,我们将指向整数的指针p转换为指向浮点数的指针q。需要注意的是,我们先使用unsafe.Pointerp转换为void*类型,然后将其转换为指向浮点数的指针类型。

  1. 类型别名

类型别名是将一个类型定义为另一个名称。例如,将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的区别

newmake都用于创建变量,但它们有一些区别:

  • 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)来实现多线程操作。确保正确地同步并发操作,以避免竞争条件和死锁

  1. 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)
}
  1. 通道(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)
	}
}

为了避免竞争条件和死锁,可以采用以下策略:

  1. 使用通道进行同步:通过在goroutine之间传递数据,而不是共享内存,可以避免竞争条件。通道可以确保数据在不同的goroutine之间安全地传输。

  2. 使用互斥锁(Mutex):当需要共享内存时,可以使用互斥锁来确保同一时间只有一个goroutine访问共享资源。Go标准库中的sync.Mutex提供了这个功能。

  3. 使用sync.WaitGroup:sync.WaitGroup可以用来等待一组goroutine完成。它可以确保所有的goroutine都完成后,主程序才继续执行,从而避免死锁。

  4. 使用select语句:select语句可以用来同时处理多个通道操作。它可以防止死锁,因为它会在多个通道中选择一个可用的操作来执行。

通过使用这些策略,Go语言可以帮助您更轻松地编写并发程序,同时避免竞争条件和死锁。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。