有基础转Go语言学习笔记(1. 基本语法篇)
有基础转Go语言学习笔记(1. 基本语法篇)
1. Go安装和Go模块
Go语言环境配置
- 安装Go语言:首先从Go官网下载并安装Go语言。
- 配置
GOROOT
:这是Go语言安装的目录。大多数情况下,它在安装时自动设置。对于自定义安装路径,需要在系统环境变量中设置GOROOT
。 - 配置
GOPATH
:GOPATH
是Go语言的工作目录,用于存放Go项目和第三方库。它通常指向用户的一个特定目录,如$HOME/go
(Unix系统)或C:\Users\用户名\go
(Windows)。 - 更新环境变量:将Go的二进制文件目录添加到系统的
PATH
环境变量中,以便可以从任何地方运行Go命令。 - 重启和验证:重启终端或计算机后,通过运行
go version
来验证Go环境是否正确配置。
Go模块(自Go 1.11起)
- 模块概念:Go模块是Go语言的包管理和依赖管理系统。一个模块是一组版本化的Go包的集合,它记录了其依赖项。
- 初始化模块:通过在项目根目录运行
go mod init [module path]
来创建一个新的模块。这将生成一个go.mod
文件,用于追踪依赖。 - 依赖管理:当你导入新的包并运行
go build
或go test
时,Go会自动添加新的依赖到go.mod
文件,并且生成go.sum
文件以确保依赖项的完整性。 - 版本控制:Go模块支持语义版本控制。在
go.mod
文件中,可以指定依赖包的具体版本。 - 模块代理:Go 1.13引入了模块代理的概念,它提供了一个中央服务器来缓存公共模块,加快构建速度和提高模块获取的可靠性。
- GOPATH与模块:使用Go模块后,
GOPATH
的重要性降低。Go代码可以存放在系统的任何位置,不再限制于GOPATH
内。
2. 基本语法
Hello World
package main
func main() {
println("Hello, world!")
}
直接运行go,不生成二进制文件:
go run helloworld.go
编译运行go:
go build helloworld.go
./helloworld.exe
变量赋值
在Go语言中,变量的声明和赋值可以通过几种方式进行:
-
默认值:未初始化的变量有默认值(如
int
为0
)。var a int
-
显式初始化:声明时指定值。
var b int = 100
-
类型推导:编译器根据赋值推断类型。
var c = 100
-
简短声明:使用
:=
,仅在函数内部。f := 100
-
多变量声明:同时声明多个变量。
var d, e = 100, "hello"
-
多行声明:组织多个变量声明。
var ( g = 100 h = "hello" )
常量和Iota
在Go语言中,常量的声明和使用 iota
的方法如下:
常量声明
Go中的常量是不可修改的值。常量可以是字符、字符串、布尔或数值类型。
-
单个常量声明:
const Pi = 3.14
-
多常量声明:
const ( StatusOK = 200 NotFound = 404 )
使用 Iota
iota
是Go语言的一个特殊常量,用于在 const
关键字出现时初始化为0,然后在每新声明一个常量时递增。
-
基本使用:
const ( Zero = iota // 0 One // 1 Two // 2 )
-
跳过某些值:
const ( _ = iota // 跳过0 First // 1 Second // 2 )
-
复杂表达式:
const ( A = 1 << iota // 1 << 0 = 1 B // 1 << 1 = 2 C // 1 << 2 = 4 D = 2 * iota // 2 * 3 = 6 E // 2 * 4 = 8 )
iota
在枚举类型的创建中非常有用,因为它允许你创建一系列相关的常量,而不需要显式地为每个常量指定一个值。在每个新的 const
块中,iota
都会重置为0。
函数和返回值
在Go语言中,函数是基本的代码组织和执行单元。函数可以有多种返回值的写法,以下是Go函数及其返回值的三种常见写法:
1. 基本函数
基本的函数声明包括函数名、参数列表和返回类型。
func Add(a int, b int) int {
return a + b
}
- 这是一个简单的函数,接收两个
int
类型的参数,并返回它们的和。
2. 返回值命名
在Go中,你可以给返回值命名,并在函数内部直接操作这些命名变量。
func Swap(x, y string) (a, b string) { // (a string, b string) 也可
a = y
b = x
return
}
- 这个函数交换两个字符串的值。返回值在声明时已经命名为
a
和b
,因此在return
语句中不需要再次指定。
3. 多返回值
Go支持函数返回多个值,这在错误处理中特别有用。
func Divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, errors.New("division by zero")
}
return a / b, nil
}
- 这个函数返回两个值:除法的结果和一个错误信息。如果除数为0,则返回错误。
4. 异常处理
package main
import (
"errors"
"fmt"
)
// Divide 函数计算两个浮点数的除法
func Divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
// 调用 Divide 函数
result, err := Divide(10.0, 0.0)
// 检查是否有错误返回
if err != nil {
fmt.Println("Error:", err)
return
}
// 打印结果
fmt.Println("Result:", result)
}
包(package)
包的路径和init()
若文件结构如下:
main.go:
package main
import (
"fmt"
_ "GoPlayground/InitLib1"
_ "GoPlayground/InitLib2"
)
func init() {
fmt.Println("libmain init")
}
func main() {
fmt.Println("libmian main")
}
被导入的包首先会被调用init方法,再递归调用它所依赖的包的init方法
包的别名
在Go语言中,不同类型的包别名使用可以通过以下代码块进行概括:
1. 普通别名
import fm "fmt"
func main() {
fm.Println("Hello, world!") // 使用别名fm调用fmt包中的Println函数
}
这里,fmt
包被赋予了别名 fm
,在代码中通过 fm
来引用 fmt
包的内容。
2. 点(.
)别名
import . "fmt"
func main() {
Println("Hello, world!") // 直接调用Println,无需包名前缀
}
使用点(.
)作为别名后,可以直接调用 fmt
包中的函数,无需使用包名作为前缀。
3. 下划线(_
)别名
import _ "database/sql"
func main() {
// 这里不直接使用database/sql包
// 但该包的init函数会被执行
}
这里,database/sql
包被导入,但不直接使用其中的任何标识符。它的 init
函数仍然会被执行,这通常用于初始化目的,如注册数据库驱动。
包的创建注意事项
作为被 import
的包,在Go语言中对外提供函数时,需要注意以下几个关键点:
1. 公开性(Exporting)
- 首字母大写:在Go中,只有首字母大写的函数、类型、变量等才能被其他包访问。这被称为“导出”(Exporting)。例如,
func MyFunction()
可以被外部包访问,而func myFunction()
则不可以。
2. 明确的函数签名
- 清晰的参数和返回类型:确保函数的参数和返回类型是清晰并且一致的。这有助于使用者了解如何正确使用你的函数。
- 文档注释:为公共函数编写文档注释是一个好习惯。这些注释应该简洁地描述函数的功能、参数、返回值以及任何副作用或特别的行为。
3. 错误处理
- 返回错误:在Go中,优雅的错误处理是通过返回一个
error
类型的值来实现的。如果函数可能遇到一个应该由调用者处理的错误情况,它应该返回一个error
类型的值。
4. 命名约定
- 遵守Go的命名约定:使用驼峰式命名(CamelCase)而不是下划线分隔。例如,使用
ComputeAverage
而不是compute_average
。
5. 最小化外部依赖
- 减少依赖:尽量减少模块对外部包的依赖。这可以减少使用你的模块时可能出现的依赖冲突。
6. 稳定的API
- 保持API稳定性:一旦你的包被其他人使用,频繁地更改公共API可能会导致兼容性问题。在更改API时要谨慎。
包的使用注意事项
在Go语言中使用包时,有几个重要的注意事项需要考虑,以确保代码的可读性、维护性和性能:
1. 选择合适的包
- 标准库优先:优先使用Go标准库中的包,因为它们经过严格测试,性能和可靠性都得到保证。
- 谨慎选择第三方包:选择社区认可度高、维护活跃的第三方包,注意检查其许可证、文档和社区支持。
2. 导入包的方式
- 避免不必要的导入:只导入你需要的包,未使用的导入会增加编译时间并潜在地增加最终二进制文件的大小。
- 使用完全限定的导入路径:避免使用相对路径导入包,这可能导致依赖管理混乱。
3. 包的命名和组织
- 简洁明了的包名:包名应简短、有描述性,避免使用下划线或混合大小写。
- 单一职责原则:每个包应只承担单一职责,这有助于维护和重用代码。
- 公开接口最小化:只公开(首字母大写)必要的类型和函数,减少包的外部API。
4. 管理包依赖
- 使用Go模块:自Go 1.11以来,使用Go模块来管理依赖是官方推荐的方式。
- 小心更新依赖:更新依赖时要小心,尤其是涉及到大的版本更改,因为这可能会引入不兼容的更改。
5. 包的初始化
- 谨慎使用init函数:
init
函数在包导入时自动执行,应当谨慎使用,避免复杂的初始化逻辑,因为这会增加代码的隐式行为和启动时间。
6. 性能考虑
- 注意导入包的性能影响:一些包可能会显著增加编译时间或运行时开销,特别是那些依赖大量资源或进行复杂初始化的包。
包和模块的区别
在Go语言中,包(Packages)和模块(Modules)是两个不同的概念,它们在组织和管理代码方面扮演不同的角色:
包(Packages)
- 基本单位:包是Go代码的基本组织单位。一个包由位于同一目录下的一个或多个
.go
文件组成。 - 命名空间:每个包提供了一个命名空间来组织其内容(如函数、结构体、接口等)。
- 导入机制:包可以被其他包导入,以使用其中定义的公共(首字母大写的)标识符。
- 目录结构:包的物理结构与文件系统的目录结构紧密相关。一个目录下的所有文件都被视为同一个包的一部分。
- 作用:包用于逻辑上划分代码,使代码易于管理和维护。例如,
fmt
和net/http
都是标准库中的包。
模块(Modules)
- 高级依赖管理:模块是Go 1.11中引入的,用于支持版本控制和包依赖管理。
- 包的集合:一个模块是一个或多个包的集合。一个模块包含一个
go.mod
文件,该文件定义了模块的根目录。 - 版本控制:模块使开发者能够指定依赖的确切版本,这有助于确保项目依赖的一致性和可重复性。
- 独立于GOPATH:模块的使用意味着可以在
GOPATH
之外的任意目录中工作,这是之前Go项目中不支持的。 - 包管理:模块提供了对外部包的依赖管理,包括版本控制和可重复的构建。
总结
- 包是代码组织的基本单位,用于划分和封装逻辑。
- 模块是一种更高级的机制,用于管理包及其依赖,包括版本控制。
在实际应用中,通常会有一个模块包含多个包。模块为这些包提供了一个统一的版本管理和依赖解析框架。
指针
对于有C/C++背景的程序员来说,Go语言中的指针概念会比较容易理解,但也有一些重要的差异需要注意:
相似之处
-
基本概念:和C/C++一样,Go中的指针代表内存地址,可以用于间接引用或操作数据。
-
语法:使用
&
操作符获取变量的地址,使用*
操作符访问指针指向的值。例如:var a int = 10 var p *int = &a *p = 20 // 改变a的值
差异
- 无指针算术:Go中不支持指针算术,即不能像在C/C++中那样对指针进行加减操作。这是一个设计决策,旨在提高程序的安全性和可维护性。
- 垃圾回收:Go是一个具有垃圾回收机制的语言,这意味着内存的分配和回收是自动管理的。因此,不需要像在C/C++中那样手动管理内存。
- 安全性:Go的指针更加安全,因为不允许直接操作内存,也没有指针算术。这减少了内存泄漏和缓冲区溢出等常见错误。
- new和make:Go提供了
new
和make
两个内建函数来分配内存。new(T)
为类型T分配零值内存并返回指向它的指针。make
用于创建切片、映射和通道,并返回一个有初始值(非零)的T类型,而不是*T。
示例
var x int = 1
var p *int = &x // p是指向int类型的指针
fmt.Println(*p) // 输出1,*p表示指针p指向的内存中的值
defer语句
在Go语言中,defer
语句用于安排随后对函数的调用。这意味着你可以在函数执行到某个点后,指定一些操作在函数返回前完成。这在处理资源释放、解锁以及进行一些清理工作时特别有用。对于有C/C++背景的程序员来说,可以将 defer
看作是一种确保在函数退出时执行特定代码的机制,类似于C++中的析构函数或Java中的final
基本使用
- 延迟调用:
defer
后的函数调用会被延迟执行,直到包含它的函数执行完毕。 - 堆栈行为:如果有多个
defer
语句,它们会按照后进先出(LIFO)的顺序执行,即最后一个defer
语句将首先执行。
func example() {
defer fmt.Println("deferred call in example")
fmt.Println("doing some work in example")
// 当example函数返回时,先执行defer语句
}
> doing some work in example
> deferred call in example
应用场景
- 资源清理:确保文件、网络连接等资源在不再需要时被正确关闭。
- 解锁互斥量:在获取互斥量后,使用
defer
解锁可以避免死锁。 - 错误处理:配合命名返回值,
defer
可用于修改函数的返回值。
错误处理示例
func readFile(filename string) (content string, err error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close() // 确保在退出函数前关闭文件
// ... 文件读取操作 ...
return string(fileContent), nil
}
在这个示例中,无论函数因为哪个路径返回,file.Close()
都将被执行,确保了文件资源被释放。
注意事项
- return和defer的先后:
return
先执行,defer
后执行 - 参数立即求值:
defer
语句中的函数参数会立即求值,而非在实际执行时求值。 - 避免过多使用:虽然
defer
很有用,但在循环或频繁调用的函数中过多使用可能会导致性能问题。 - 错误检查:延迟执行的函数通常也会有返回值,例如用于错误检查,但这些返回值通常会被忽略。可以通过命名返回值或其他方式来处理这些错误。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!