Go语言基础篇
环境配置
windows直接安装
然后进入环境变量中设置GOPATH和GOBIN
GOPATH是Go语言的工作目录,Go语言程序都要放入其中才能执行
GOBIN是Go编译生成的程序的安装目录,比如go install会将上传的Go程序安装到Gobin中。
所以将GOPATH环境变量设置成工作目录,环境变量设置为下载go的那个bin中
编译器用Goland
数据类型
总的来说语言都一样
主要就是定义的语法
go中和python一样也可以直接分配
var s string ="good"
或者
s:="good"
指针:
pi:=&i
fmt.println(*pi) 输出和i一样
iota:
常量生成器,它可以用来初始化相似规则的常量,避免重复的初始化。
不使用iota
const(
one=1
two=2
three=3
four=4
)
使用iota
const(
one=iota+1
two
three
four
)
fmt.Println(one,two,three,four)
字符串
//字符串和数字互转
func main() {
i := 484454
is2 := strconv.Itoa(i) //数字转字符串
si2, err := strconv.Atoi(is2) //字符串转数字
fmt.Println(i, is2, si2, err)
}
//同样还有浮点数转字符串,布尔型转字符串等
//强制转换
func main() {
i := 45454.445
i2f := float64(i)
f2i := int(i2f)
fmt.Println(i, i2f, f2i)
}
控制语句
go的控制语句给人的感觉就像c语言和python混合起来的东西
if条件语句
大部分和python一样
但是可以直接在if中定义
func main() {
if i := 6; i > 10 {
fmt.Println("flag")
} else {
fmt.Println("lsl")
}
}
Switch语句
特殊的switch十分灵活,可以直接写语句判断
func main() {
switch 5 > 6 {
case true:
fmt.Println("true")
case false:
fmt.Println("false")
}
for语句
可以使用c语言一样使用go的for循环,也可以和python一样使用
go中没有while循环,但是可以用for达到while的效果
遍历数组
func main() {
array:=[5]int{1,2,3,4,5}
for i:=range array {
fmt.Println(i)
}
}
array&slice&map
数组
在Go中数组的长度也是数组类型的一部分,比如[5]string和[4]string是俩个不同的类型。
如何定义
固定长度
array:=[5]string{'a','b','c','d','e'}
省略长度-只适用所以元素初始化的时候
array:=[...]string{'a','b','c'}
数组循环
func main() {
array := [5]int{1, 2, 3, 4, 5}
for _,j := range array {
fmt.Printf("%d:\n", j)
}
}
在Go中可以使用_来丢弃参数
切片(Slice)
和数组很像,可以理解为动态数组
切片不会读取最后的那一位
基于数组创建切片
func main() {
array := [...]int{1, 2, 3}
alice := array[0:2]
alice1 := array[0:] //从0到最后
array2 := array[:3] //从最前面到2
fmt.Println(alice)
fmt.Println(alice1)
fmt.Println(array2)
}
输出 [1 2]
[1 2 3]
[1 2 3]
切片的声明
切片不止可以从数组中生成,还可声明创建
切片的创建
使用make函数创建切片
func main() {
silice:=make([]string,4) //创建一个长度为4的切片
silice:=make([]string,4,8) //创建一个长度为4,容量为8的切片
切片的容量不能比长度少
}
直接创建
slices:=[]string("a","b","c","d","e")
切片中的长度就是原素个数,切片的容量是切片的空间,当我们要增加长度时就会增加到空的空间中,当长度要超过容器时就会进行扩容
append函数追加切片元素
func main() {
silice1 := []string{"a", "b", "c", "d", "e"}
silice2 := []string{"a", "b", "c", "d", "e"}
silice3 := append(silice1, "f") //追加一个元素或多个元素
silice4 := append(silice1, silice2...) //追加一个切片
fmt.Println(len(silice1), cap(silice1))
fmt.Println(len(silice2), cap(silice2))
fmt.Println(silice3)
fmt.Println(silice4)
}
映射(map)
在Go中map就和java,python中的字典一样
创建和初始化map
//使用make函数创建
func main() {
mapp := make(map[string]int) //其中key类型为string,value类型为int
mapp["folly"]=20
}
//直接创建
func main() {
mapp := map[string]int{"folly":20}
}
获取和删除map
和其他语言的差不多
但是操作符有区别
func main() {
mapp := map[string]int{"folly": 20}
god, err := mapp["folly"]
if err {
fmt.Println(god)
}
}
主要err的作用就是判断是否存在follycat这个key
//删除操作
delete(mapp, "folly")
遍历
和python的差不多,用for-range
func main() {
mapp := make(map[string]int)
mapp["flfll"] = 20
mapp["fofof"] = 15
mapp["fjfjjf"] = 45
for i, j := range mapp {
fmt.Println("Key", i, "Value", j)
}
}
string和[]byte
在Go中可以直接转化为byte
func main() {
s := "Hello 我吧了了"
bs := []byte(s)
fmt.Println(bs)
fmt.Println(s[0], s[15])
}
其中汉字占位为3个字节,所以15存在。
函数和方法
函数
函数声明
格式
func funcName(params) result{
body
}
创建一个add函数,
func add(a, b int) int {
return a + b
}
func main() {
a := 10
b := 52
fmt.Println(add(a, b))
}
多值返回
与java等语言不同,Go语言可以返回多个返回值,也就是可以多值返回。
func sum(a, b int) (int, error) {
if a < 0 || b < 0 {
return 0, errors.New("a或b不能为负数")
}
return a + b, nil
}
func main() {
a := -8
b := 52
num, err := sum(a, b)
fmt.Println(num, err)
}
返回值命名
我们可以为返回值命名一个名字,然后可以和参数一样使用。
func sum(a, b int) (sum int, err error) { //命名返回值
if a < 0 || b < 0 {
return 0, errors.New("a或b不能为负数")
}
sum = a + b
err = nil
return
}
可变参数
可变参数就是函数的参数数量是可变的,比如常见的fmt.Println函数
同一个函数,可以不传参数,可以传一个参数,也可以传很多个参数,这种函数就是具有可变参数的函数。
fmt.Println()
fmt.Println("folly")
fmt.Println("LC","RC","SEC")
可以跳转到Println的定义,我们可以看到只要在参数类型前加三个点就行
构造sum函数
func sum(params ...int) int {
sum := 0
for _, i := range params {
sum += i
}
return sum
}
func main() {
a := -8
b := 52
num := sum(a, b)
fmt.Println(num)
}
tip:如果定义的函数中既有可变参数又有普通参数,可变参数一定要放在参数列表的末尾。
包级函数
和java,python中的那个包差不多
匿名函数和闭包
匿名函数就是没有名字的函数
func main() {
sum1 := func(a, b int) int {
return a + b
}
fmt.Println(sum1(1, 3))
}
其中sum1就是匿名函数
有了匿名函数,就可以实现函数中定义函数了,在函数内定义的匿名函数就叫内部函数,并且内部函数可以使用外部变量,也就是闭包。
func main() {
c1 := add()
fmt.Println(c1())
fmt.Println(c1())
fmt.Println(c1())
}
func add() func() int {
i := 0
return func() int {
i++
return i
}
}
c1被我们赋值为add函数,而c1就是内部函数,所以c1函数有外部函数add的变量i,所以每调用一次i都会加1
方法
方法的定义
在Go语言中方法和函数是俩个概念。方法必须有一个接收者,这个接收者是一个类型,方法就会和这个类型绑定,称为这个类型的方法。
type Age uint //定义一个新类型Age,也就是对uint重命名
func (age Age) String() { //接收Age类型的参数,方法为String()
fmt.Println("age is",age)
}
func main() {
age :=Age(55) //定义Age类型的age参数
age.String() //调用String方法
}
值和指针类型接收者
方法的接收者除了可以是值类型,也可以是指针类型。
如果定义的方法的接收者类型是指针,那么对指针的修改就是有效的,如果不是指针,修改就是无效的。
type Age uint
func (age Age) String() {
fmt.Println("age is", age)
}
func (age *Age) Modify() {
*age = Age(30)
}
func main() {
age := Age(55)
age.String()
age.Modify()
age.String()
}
tip:
在调用方法时,传递的接收者本质上都是副本,只不过一个是这个值的副本,一个是指向这个值的指针的副本。指针具有指向原有值的特性,所以修改了指针指向的值,也就修改了原有的值。也就是说值接收者使用的是值的副本来调用方法,而指针接收者使用实际的值来调用方法。
上面用的就是值类型的变量,不是指针类型,使用指针也可以
(&age).Modify()
struct和interface:隐式的接口实现
结构体的定义
和c语言的很像,就是一种聚合类型,其中可以包含任意类型的值,这些值就是我们定义的结构体成员,也就是字段。
type person struct { //定义person结构体
name string //string类型的name
age int //int类型的age
}
在定义结构体时差不多都是这个格式
结构体的声明和使用
结构体类型也可以使用与普通的字符串和整型一样的方式进行声明和初始化。
type person struct {
name string
age int
}
func main() {
var p person
pp := person{"cat", 55}
fmt.Println(p)
fmt.Println(pp.name, pp.age)
}
第一个输出为默认值,第二个为我们定义的值
Go中调用名字啥的和其他语言一样都是用.
当然,如果不想按顺序输入数据也可以直接指出名字
ppp:=person{age:55,name: "folly"}
结构体的字段
结构体的字段可以为任何类型,包括自定义的结构体
type person struct {
name string
age int
}
type student struct {
p person
school string
}
func main() {
p:=student{p:person{name:"cat",age: 55},school: "billi"}
fmt.Println(p.p.name,p.p.age,p.school)
}
输出也和上面调用一样
接口
接口的定义和实现
接口是与调用方的一种约定,它是一个高度抽象的类型,不用与具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以干什么,但调用方不用知道它的内部实现。
接口关键词为interface。
type person struct {
name string
age int
}
type Stringer interface { //创建一个接口
String() string
}
func (p person) String() string { //实现接口方法
return fmt.Sprintf("%s is %d", p.name, p.age)
}
func printfl(s fmt.Stringer) { //使用函数利用接口方法
fmt.Println(s.String())
}
func main() {
p := person{"fooo", 55}
printfl(p)
}
可以看到在实现接口方法时,这个方法和接口里的方法签名(名称,参数,返回值)一样,这样结构体就实现了Stringer接口,并且接口的实现没有通过任何关键字,所以Go语言的接口是隐式实现的。
因为printfl函数是面向接口编程的,只要一个类型实现了stringer接口就可以打印出对应字符串。
type person struct {
name string
age int
}
type school struct {
add string
}
type Stringer interface {
String() string
}
func (p person) String() string {
return fmt.Sprintf("%s is %d", p.name, p.age)
}
func (a school) String() string {
return fmt.Sprintf("the scholl add is %s", a.add)
}
func printfl(s fmt.Stringer) {
fmt.Println(s.String())
}
func main() {
p := person{"fooo", 55}
s := school{"billi"}
printfl(p)
printfl(s)
}
和上面的代码比起来,添加了一个school结构体和实现school的接口方法,同样也调用printfl函数,也是可以使用的。
所以,接口的优势就是,只要定义和调用满足双方约定,就可以使用,不用考虑具体类型的实现。
值和指针类型接收者
在go语言中,方法的值类型接收者和指针类型接收者由于编译器会自动进行转化,所以可以看成等价的,但是在接口中,值和指针接收者不一样。
如果将上面的代码输出更改为printfl(&p)并不会报错,但是将接收者改为指针 p *person时,printfl§时会报错,这说明了指针类型的接收者没有实现值接收者。
方法接收者 实现接口类型
(p person) person 和*person
(p *person) 只有*person
工厂函数
工厂函数一般用于创建自定义的结构体,便于使用者调用。
type person struct {
name string
age int
}
func Newperson(name string) *person {
return &person{name: name}
}
定义一个工厂函数newperson,它接收了string类型参数,同时返回一个*person。
通过工厂函数创建自定义结构体的方式,可以让调用者不用关注结构体内部字段,只要给工厂函数传参就行。
并且,工厂函数也可以用来创建一个接口,它可以隐藏内部具体类型的实现,让调用者只要关注接口的使用即可。
// 定义一个接口
type Animal interface {
Speak() string
}
// 实现接口的具体类型
type Dog struct{}
type Cat struct{}
// Dog 的 Speak 方法实现
func (d Dog) Speak() string {
return "Woof!"
}
// Cat 的 Speak 方法实现
func (c Cat) Speak() string {
return "Meow!"
}
// 工厂函数,根据传入的类型返回相应的 Animal 接口实例
func AnimalFactory(animalType string) Animal {
switch animalType {
case "dog":
return Dog{}
case "cat":
return Cat{}
default:
return nil
}
}
func main() {
// 使用工厂函数创建不同的动物实例
dog := AnimalFactory("dog")
cat := AnimalFactory("cat")
// 调用它们的 Speak 方法
if dog != nil {
fmt.Println("Dog says:", dog.Speak())
}
if cat != nil {
fmt.Println("Cat says:", cat.Speak())
}
}
继承和组合
Go语言和python,java等不同的是Go语言中没有继承这一概念,所以结构体,接口之间也没有继承关系,Go语言中用的是组合,利用组合达到代码复用的目的,更加灵活。
type Reader interface {
Read(p []byte) (n int ,err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
上面俩个接口组合成了下面那个接口,也就是说ReadWriter就是Reader和Writer组合,也就是有俩个接口的全部方法
并且结构体也可以组合
type person struct {
name string
age int
}
type student struct {
person
school string
}
直接放进去就是组合
类型断言
有了接口和实现接口的类型,就会有类型断言。类型断言用于判断一个接口的值是否是实现该接口的某个具体类型,并且可以实现动态类型检查。
value := interfaceValue.(ConcreteType)
其中如果interfaceValue的实际类型为ConcreteType,那么断言成功,value将包含接口的具体值,如果断言失败就会导致运行错误
安全的进行类型断言
value, ok := interfaceValue.(ConcreteType)
这种情况下,如果 interfaceValue的实际类型是 ConcreteType,ok 将为 true,value 将包含该值。如果不是,ok 将为 false,value将为该类型的零值。
// 定义一个接口
type Animal interface {
Speak() string
}
// 实现接口的具体类型
type Dog struct{}
type Cat struct{}
// Dog 的 Speak 方法实现
func (d Dog) Speak() string {
return "Woof!"
}
// Cat 的 Speak 方法实现
func (c Cat) Speak() string {
return "Meow!"
}
// 一个接受 Animal 接口的函数
func DescribeAnimal(a Animal) {
// 使用类型断言检查具体类型
switch animal := a.(type) {
case Dog:
fmt.Println("This is a Dog. It says:", animal.Speak())
case Cat:
fmt.Println("This is a Cat. It says:", animal.Speak())
default:
fmt.Println("Unknown animal.")
}
}
func main() {
// 创建 Dog 和 Cat 实例
dog := Dog{}
cat := Cat{}
// 描述它们
DescribeAnimal(dog)
DescribeAnimal(cat)
// 示例安全的类型断言
var animal Animal = dog
if dogValue, ok := animal.(Dog); ok {
fmt.Println("Successfully asserted to Dog:", dogValue.Speak())
} else {
fmt.Println("Failed to assert to Dog")
}
if catValue, ok := animal.(Cat); ok {
fmt.Println("Successfully asserted to Cat:", catValue.Speak())
} else {
fmt.Println("Failed to assert to Cat")
}
}
错误异常处理
错误
在Go语言中如果错误是可预期的,并且不是特别严重不会影响程序的运行。
error接口
在go语言中,错误是通过内置的error接口表示的。它只有一个error方法,用于返回具体的错误信息
go中的error内置接口如下
type error interface {
Error() string
}
任何实现了Error()方法的类型都可以被视为error。这使得Go语言在处理错误时具有灵活性。
当成功执行时,函数一般返回nil,不成功时返回具体错误信息
func main() {
i, err := strconv.Atoi("a")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(i)
}
}
比如用内置函数Atoi将a转化为整数就会输出错误信息
error工厂函数
除了可以调用go语言自带的error的错误信息之外,这个错误信息在我们自己写的函数中可以自己定义
func performTask(success bool) error {
if !success {
return errors.New("task failed")
}
return nil
}
func main() {
err := performTask(false)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Task completed successfully")
}
}
这个就会返回我们自己定义的错误信息
自定义error
当然返回错误信息不止能传递一个字符串,如果想有更多信息返回那么就可以自定义error函数
自定义错误类型可以通过定义一个结构体并实现error接口进行完成。
package main
import (
"fmt"
)
// MyError 定义自定义错误类型
type MyError struct {
Msg string
Code int
}
// Error 实现 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("Code %d: %s", e.Code, e.Msg)
}
// 模拟一个可能出错的函数
func doSomething(success bool) error {
if !success {
return &MyError{Msg: "operation failed", Code: 400}
}
return nil
}
func main() {
err := doSomething(false)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Operation completed successfully")
}
}
可以返回状态码,以及出现的错误
error断言
在Go语言中,错误断言是指通过类型断言来检查和处理特定类型的错误。这种机制允许开发者判断一个接口类型的错误值是否是某个具体的自定义错误类型,从而执行相应的处理逻辑。
可以在之前的代码上添加断言语句
package main
import (
"fmt"
)
// MyError 自定义错误类型
type MyError struct {
Msg string
Code int
}
// Error 实现 error 接口
func (e *MyError) Error() string {
return fmt.Sprintf("Code %d: %s", e.Code, e.Msg)
}
// 模拟一个可能出错的函数
func doSomething(success bool) error {
if !success {
return &MyError{Msg: "operation failed", Code: 400}
}
return nil
}
func main() {
err := doSomething(false)
if err != nil {
// 使用类型断言检查错误类型
if myErr, ok := err.(*MyError); ok {
fmt.Printf("Custom Error: %s with code %d\n", myErr.Msg, myErr.Code)
} else {
fmt.Println("An unexpected error occurred:", err)
}
} else {
fmt.Println("Operation completed successfully")
}
}
可以看到在代码中,使用类型断言来判断错误是否为MyError型,然后用if判断对错。
错误嵌套
在Go语言中,错误嵌套是一个重要的错误处理模式,允许将一个错误封装在另一个错误中,从而提供更多的上下文信息。自Go 1.13版本以来,Go语言引入了errors包中的Unwrap函数和Is、As等函数,使得错误处理变得更加灵活和强大。
定义
错误嵌套的基本概念是:当你处理错误时,可以将原始错误包装到一个新的错误中,并提供额外的信息。这有助于在调试和日志记录时保留错误的原始信息。
Error Wrapping功能
在Go语言中,从1.13版本开始引入了错误包装(Error Wrapping)功能,这使得错误处理更加灵活。通过错误包装,可以将一个错误包装在另一个错误中,并在需要时访问原始错误。
正常来说,我们可以和嵌套结构体一样来嵌套error,并使用Error Wrapping直接返回所有错误信息。
package main
import (
"errors"
"fmt"
)
// 自定义错误类型
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
// 模拟一个可能出错的函数
func doSomething() error {
return &MyError{Msg: "something went wrong"}
}
// 包装错误的函数
func wrapError() error {
err := doSomething()
if err != nil {
// 使用 %w 进行错误包装
return fmt.Errorf("wrapError failed: %w", err)
}
return nil
}
func main() {
err := wrapError()
if err != nil {
// 打印包装后的错误
fmt.Println(err)
// 检查原始错误
if errors.Is(err, &MyError{}) {
fmt.Println("The error is of type MyError")
}
}
}
errors.Unwrap函数
这个函数的作用就是从我们嵌套生成的一个新的error中得到被嵌套的原始错误
一般在错误处理逻辑中,如果需要访问原始错误进行特定除了和记录时会使用它
package main
import (
"errors"
"fmt"
)
// 自定义错误类型
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
// 模拟一个可能出错的函数
func doSomething() error {
return &MyError{Msg: "something went wrong"}
}
// 包装错误的函数
func wrapError() error {
err := doSomething()
if err != nil {
// 使用 fmt.Errorf 进行错误包装
return fmt.Errorf("wrapError failed: %w", err)
}
return nil
}
func main() {
err := wrapError()
if err != nil {
// 打印包装后的错误
fmt.Println("Wrapped error:", err)
// 使用 errors.Unwrap 提取原始错误
originalErr := errors.Unwrap(err)
if originalErr != nil {
fmt.Println("Original error:", originalErr)
}
}
}
errors.ls 函数
这个函数主要作用就是一个错误是不是特定类型的错误,因为当我们嵌套error后得到一个新的error方法,可以判断我们这个错误是不是在那个错误中
在复杂的错误处理中,可以通过 errors.Is 确定错误的类型或分类。
package main
import (
"errors"
"fmt"
)
// 自定义错误类型
var ErrMyError = errors.New("my custom error")
func doSomething() error {
return ErrMyError // 模拟返回自定义错误
}
// 包装错误的函数
func wrapError() error {
err := doSomething()
if err != nil {
// 使用 fmt.Errorf 进行错误包装
return fmt.Errorf("wrapError failed: %w", err)
}
return nil
}
func main() {
err := wrapError()
if err != nil {
// 打印包装后的错误
fmt.Println("Wrapped error:", err)
// 使用 errors.Is 检查是否是特定的错误
if errors.Is(err, ErrMyError) {
fmt.Println("The error is ErrMyError")
} else {
fmt.Println("The error is not ErrMyError")
}
}
}
error.As函数
用于将错误转换为特定类型的错误。它主要用于提取错误的详细信息,特别是在处理自定义错误类型时
当需要访问错误的特定字段或方法时,errors.As 可以安全地将错误转换为对应的类型。
package main
import (
"errors"
"fmt"
)
// 自定义错误类型
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
func doSomething() error {
return &MyError{Msg: "something went wrong"} // 返回自定义错误
}
// 包装错误的函数
func wrapError() error {
err := doSomething()
if err != nil {
return fmt.Errorf("wrapError failed: %w", err)
}
return nil
}
func main() {
err := wrapError()
if err != nil {
fmt.Println("Wrapped error:", err)
// 使用 errors.As 提取特定类型的错误
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("Extracted MyError:", myErr)
} else {
fmt.Println("Error is not of type MyError")
}
}
}
defer函数
defer 是一个用于延迟执行函数或方法的关键字。
而在Go语言中,defer函数经常用于文件操作时使用,可以保证文件的关闭操作一定被执行,无论函数出现异常还是错误。
package main
import (
"fmt"
)
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
当我们执行时,可以发现先输出Hello,再World
因为defer 语句会在外围函数返回之前执行,如果有多个 defer 语句,它们会以逆序执行
当我们操作文件时,我们可以先用defer写一个关闭操作,保证我们的文件一定会被关闭
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
)
func main() {
// 打开文件
file, err := os.Open("example.txt")
if err != nil {
log.Fatal(err)
}
// 确保在函数结束时关闭文件
defer file.Close()
// 读取文件内容
data, err := ioutil.ReadAll(file)
if err != nil {
log.Fatal(err)
}
// 输出文件内容
fmt.Println(string(data))
}
panic函数和recover函数
panic函数用于引发一个运行时的错误,导致程序的异常终止,调用后会立刻停止当前函数的执行,并开始逐层返回,直到找到一个处理 panic的恢复函数(recover),或者程序最终终止。
recover函数用于从 panic 状态中恢复执行。它通常与 defer 语句结合使用,以便在发生 panic 时能够捕获并处理错误,而不是让程序直接崩溃。
tip:
- recover 只能在 defer 函数中有效,不能在普通函数调用中使用。
package main
import (
"fmt"
)
func riskyFunction() {
panic("something went wrong") // 触发 panic
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
riskyFunction() // 这里会触发 panic
fmt.Println("This line will not be executed")
}