1.go语言变量
两种声明方式 var a (int) 10 或在func内可用a:=10
2.go语言常量
const a [int] =0
常量枚举可定义多个常量
如 const(
nums1=100
nums2 //100
nums3 //100
)
其次还有iota计数器用来重置常量
const(
a=iota //0
b //1
c //2
-
d //4
)
const e =iota //const再次出现,重置为0
a="abc"
b=unsafe.sizeof(a)
此时输出b会是16,因为字符串类型对应一个结构体,该结构体有两个域,第一个域是指向该字符串的指针,第二个域是字符串的长度,每个8字节,共计1个字节,但是并不包含指向该指针的具体内容,因此sizeof(字节大小)返回一直会是16
iota的两种特殊使用:
(1)
const(
a=iota
b
c="haha"
d
e=iota
f)
此时输出0,1,haha,haha,4,5
(2)
const(
a=1<<iota
b=3<<iota
c
d
)
此时a为1左移0位,b为3左移1位,后面均为3左移iota位
故输出1,6,12,24
3.运算符
主要注意一下几个赋值运算符,&=按位与,|=按位或,^=按位异或
以及地址运算符&取地址,*指针变量
一级:*/%<<>>&&^
二级:+-|^
三级:== != <<=>>=
四级:&&
五级:||
注:^相同为0,不同为1
4.结构体
定义:type name struct{
ID int
Age int
.......}
初始化
st:=name{
ID:1000
Age:18
...}
访问结构体成员:结构体变量.成员变量,ex:st.Age
并且结构体内部可以嵌套,例如
type Geo struct{
loc string
}
type name struct{
ID int
Age int
Geo
.......}
此时再初始化st后即可调用st.loc
5.数组与切片
var nums=[10]int {}或var nums=[...]int {}(自动推断元素个数)
var nums=[]int{}或...
var nums=make([]int,2,10),第二三个分别代表初始长度(0填充),最大长度
说白了就是定没定初始长度,可不可以扩容的问题
6.条件语句: if eles if else 以及switch {case1: case2 ... default}
7.循环:golang中只有for 三种,一种经典像java一样,另外一种语法糖for i,v:=range list{},另外模仿while,for i<100...,或可以无限循环for {}
在for range 中有一种特殊的情况需要考虑,有如下代码
arr:=[]int{1,2}
nums:=[]*int{}
for i,v :=range arr{
nums=append(nums,&v)}
此时输出*nums[0]和*nums[1]
希望得到1,2
实际得到2,2
每次打印出v的地址,发现v均不会改变,即为v最后一次的地址,因为在遍历过程中实际是将遍历值存在了临时变量当中,遍历1时存1,遍历2时存2,所以输出的一直是临时变量的地址
那么如何解决这种问题呢,首先可以用一个临时变量存储v,这样输出的会是临时变量的地址,而不会总是v的地址,而v的值每次都不同,所以相当于每次添加了不同的temp地址,也就不会出现上述问题。
代码如下
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
res := []*int{}
for _, v := range nums {
temp := v
res = append(res, &temp)
}
fmt.Println(*res[0], *res[1])
}
或者是用索引来存储,而不是用值
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
res := []*int{}
for i, _ := range nums {
res = append(res, &nums[i])
}
fmt.Println(*res[0], *res[1])
}
8.函数:主要就是记住结构func main(入参)返回值{函数体},注意go是支持多个返回值的,并且在go中一切皆变量,函数可以做函数的参数,函数可以做函数的返回值
另外就是函数闭包,函数闭包=匿名函数+运行环境,函数闭包其实就是引用了一些变量的匿名函数,这些变量不会随原环境而释放,生命周期同匿名函数
注意Go中函数参数只是值传递,举个例子来说对于数组来说,函数不可以修改其中的数值,而切片就可以,因为切片其实是一个结构体,其中有一个指向切片内容的指针,函数中对切片的修改实际修改了指针,但数组就是值做参数,而不会修改相应的引用指针地址。有如下代码:
package main
import "fmt"
func change(arr []int) {
arr[0] = 100
}
func change1(arr [2]int) {
arr[0] = 100
}
func main() {
arr := [...]int{1, 2}
arr1 := []int{1, 2}
change(arr1)
fmt.Println(arr1[0])
change1(arr)
fmt.Println(arr[0])
}
匿名函数:就是没有函数名的函数,调用的时候直接使用()(就像平时调用某个函数change(arr1)),匿名函数内部可以使用全局变量,内部变量。有代码如下:
package main
import "fmt"
func main() {
logo := "hello"
func(i int) {
logo1 := "hi"
fmt.Println(i + 1)
fmt.Println(logo)
fmt.Println(logo1)
}(100)
}
注意这个()要紧跟在}之后,否则会报错
package main
import "fmt"
func test() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
getnum := test() //此时相当于调用了test,返回了该匿名函数
fmt.Println(getnum)
fmt.Println(getnum())
fmt.Println(getnum())
fmt.Println(getnum()) //此时相当于加()调用了匿名函数,返回i,并且注意,此时只调用了匿名函数,所以i不会被重置,而会一直递增,所以匿名函数的生命周期同匿名函数,而不同外部函数
getnum1 := test() //直到此时重新调用,重置i
fmt.Println(getnum1)
fmt.Println(getnum1())
}
最后还有可以将函数直接赋值给变量,称为函数变量,如下:
package main
import (
"fmt"
"math"
)
func main() {
test := func(x float64) float64 {
return math.Sqrt(x)
}
fmt.Println(test(9))
}
9.指针
go中创建指针有三种方法:var a*int,a=new(int)(此时会开辟空间,即已经有了内存地址,而第一种此时还为nil,此时取*会默认为0,后续赋值地址会变成新值地址),以及直接取地址a=&b,注意go中指针不能运算
注意,地址只能赋值给指针变量,并且对于指针变量取*,会得到该变量存储的值的地址,具体如下:
package main
import "fmt"
func main() {
var a *int
b := 3
a = &b
fmt.Println(a)
fmt.Println(*a)
}
10.方法:绑定在任一具体数据类型上的函数,数据类型可以是type定义的任意数据类型或者其对应的指针类型。
具体来说 func(p *Name) func getname () string{return p.name}这样
另外在go中,如果一个结构体想继承另一个结构体,只需要在当前结构体中写入想继承的结构体名即可,具体如下:
type People struct{
Name string
Age int
}
type Student struct{
Id int
Score int
Sex string
People
}
具体方法的示例,如下:
package main
import "fmt"
type People struct {
Name string
Age int
}
type Student struct {
Id int
Score int
Sex string
People
}
func (p *People) Getname() string {
return p.Name
}
func (s Student) Getscore() int {
return s.Score
}
func main() {
st := &Student{
Id: 1,
Score: 100,
Sex: "man",
People: People{
Name: "Gary",
Age: 20,
},
}
fmt.Println(st.Getscore())
}
在goland中测试,无论是否*或&,都能返回正确结果,但以下情况就不可以了。
要修改结构体内容时,必须用引用,原因和上面说的数组一样
package main
import "fmt"
type People struct {
Name string
Age int
}
func (p People) SetName(name string) {
p.Name = name
}
func main() {
p := People{Name: "Gary", Age: 20}
fmt.Println("Before:", p.Name) // 输出: Gary
p.SetName("John")
fmt.Println("After:", p.Name) // 输出: Gary (没有修改)
}
指针接收者通常用于以下情况:
- 方法需要修改接收者的状态。
- 拷贝大的结构体会影响性能。
- 保证一致性和避免拷贝。
值接收者通常用于以下情况:
- 方法不需要修改接收者的状态。
- 接收者是一个小的结构体。
- 你希望确保拷贝一个结构体。
不考虑实际开发情况,做题时大概率应该是使用*
11.接口:接口是一组函数的声明,如果某种数据类型绑定了这个接口中的所有函数,则称这种类型为该接口的一种实现。
结构:type name interface{
func [] ()//注意没有定义,即没有函数体,只有声明}
空接口:即接口内没有任何函数声明,因此任何类型都是空接口的实现,因此任何类型都可以赋值给空接口,即
var a interface {}
var b int=3
a=b
故此引出函数断言:
value,ok:=x.(T),,x为接口类型,ok为boool类型。若断言成功,value为x,类型为T,ok为true。否则,value为0,ok为false。这里可以这么用:
package main
import "fmt"
type Phone interface {
call()
send()
}
type Apple struct {
Name string
}
func (a *Apple) call() {
fmt.Println(a.Name + "Apple call success")
}
func (a *Apple) send() {
fmt.Println(a.Name + "Apple send success")
}
func main() {
var phone Phone//声明接口
phone = new(Apple)//实现接口,因为Apple绑定了所有接口中的函数,将*Apple类型的值赋值给接口类型Phone的变量phone。在这个过程中,Go会自动将*Apple类型的值转换为Phone接口类型。这个转换包括存储指向具体类型实例的指针以及类型信息,使得接口变量能够在需要时调用具体类型的方法。
phone.(*Apple).Name = "Apple"//使用断言赋值,过程是,phone通过new变成了*Apple类型,然后在这里判断是否和*Apple相等,相等将转换,
//类型Phone
不知道它持有的具体值的类 型,所以不能直接访问具体类型的字段。为了访问具 体类型Apple
的Name
字段,我们需要使用类型断言将接口变量转换为具体类型,而第一步实例化结束,还是接口类型,需要具体类型转化 phone.call() }
其实上面这种断言方式比较容易出错,还有一种形式是:
package main
import (
"fmt"
)
func main() {
var phone interface{}
var a = 0
var b *int
phone = b
value, ok := phone.(*int)
fmt.Println(value, ok)
value1, ok1 := phone.(int)
fmt.Println(value1, ok1)
fmt.Println(a)
}
同一个数据结构,可以实现不同的接口,如下所示:
package main
import "fmt"
type Myread interface {
Myreader()
}
type Mywriter interface {
Mywriter()
}
type Myreadwriter struct {
}
func (r *Myreadwriter) Myreader() {
fmt.Println("有输入功能")
}
func (w *Myreadwriter) Mywriter() {
fmt.Println("有输出功能")
}
func main() {
var myread Myread
myread = new(Myreadwriter)
myread.Myreader()
var mywriter Mywriter
mywriter = new(Myreadwriter)
mywriter.Mywriter()
}
接口还可以做函数参数,如:
package main
import "fmt"
type Reader interface {
Add() int
}
type nums struct {
a, b int
}
func (n *nums) Add() int {
return n.a + n.b
}
func Dojob(reader Reader) {
fmt.Printf("num is %d\n", reader.Add())
}
func main() {
num := &nums{1, 2} //实现了一个nums类型的接口,因为nums为Reader的一个实现,所以可以在函数Dojob中,作为实参传入
Dojob(num)
}
接口还可以嵌套使用,但是要实现被嵌套的接口,需要实现接口中的全部方法,如下所示:
package main
import "fmt"
type A interface {
run1()
}
type B interface {
run2()
}
type C interface {
A
B
run3()
}
type run struct {
}
func (r *run) run1() {
fmt.Println("run1已实现")
}
func (r *run) run2() {
fmt.Println("run2已实现")
}
func (r *run) run3() {
fmt.Println("run3已实现")
}
func main() {
var test C
test = &run{} //run 实现了C的全部方法,如果有一个方法没实现,就不能执行,因为C类型接口中的方法没有全部实现
test.run1()
test.run2()
test.run3()
}
12.error:Go语言中的error是一个普通的接口类型,用来表示程序执行过程中的错误信息。
type error interface{
Error() string
}
error对象的创建上:
1.eerrors.New(msg string)//导入errors包
2.fmt.Errorf(msg string)//导入fmt包
用处:一般用在多返回值函数的最后一个返回值,表示函数执行出错的错误信息,一般用if语句判断一下,出错了怎么办,不出错怎么办,如下:
package main
import (
"errors"
"fmt"
)
func increself(i int) (int, error) {
if i > 0 {
return i + 1, nil
}
return i, fmt.Errorf("输入错误")
}
func main() {
fmt.Println(increself(10))
fmt.Println(increself(-1))
error2 := errors.New("hello")
error3 := errors.New("hello")
fmt.Println(error2 == error3) //这里是不相等的,因为在errors.New的源码中,返回的是hello的地址,是不相同的
fmt.Println(error2.Error() == error3.Error()) //相同,因为Error()方法返回的是string,两个字符串是一样的,所以两个error比较不能直接比较,而是应该比较Error()
}
但是有些时候,我们可能不止需要返回错误信息,我们可能还想知道错误码,如error code,所以很多时候我们可以自定义error对象,而error是一个接口,所以我们可以自定义结构体,然后绑定到Error方法,这样就可以实现自定义错误返回,如下:
package main
import (
"fmt"
)
type MyError struct {
msg string
code int
}
type MyError2 struct {
msg1 string
code1 int
}
func (m MyError) Error() string {
return fmt.Sprintf("msg is %s, code is %d", m.msg, m.code)
}
func (m2 MyError2) Error() string {
return fmt.Sprintf("msg1 is %S,code1 is %D", m2.msg1, m2.code1)
}
func Trans(msg string, code int) MyError {
return MyError{msg: msg, code: code}
} //直接将输入的信息转换格式
func Trans2(msg2 string, code2 int) MyError2 {
return MyError2{msg1: msg2, code1: code2}
}
func code(err error) int {
if value, ok := err.(MyError); ok {//这里用断言做判断,同样的情况下,MyError2也实现了Error方法从而实现了error接口,但如果这里是用MyError2做断言,main函数中就无法执行出正确信息
return value.code
}
return -1
}
func msg(err error) string {
if value, ok := err.(MyError); ok {
return value.msg
}
return "错误的信息"
}
func main() {
error := Trans("hello world", 0)
fmt.Println(code(error))
fmt.Println(msg(error))
}
13.defer
defer主要用在函数调用或方法调用前,延迟函数的执行。
延迟到什么时候:1.发生return 2.发生panic 通俗地讲,函数正常或异常退出的时候
语法规则:1.defer后必须是函数或方法调用 2.延迟内容不能被括号括起来
多个defer的执行顺序:LIFO 先调用 后执行,演示如下:
package main
import "fmt"
func defer1() {
fmt.Println("defer1")
}
func defer2() {
fmt.Println("defer2")
}
func defer3() {
fmt.Println("defer3")
}
func main() {
defer defer1()
defer defer2()
defer defer3()
}//输出顺序为3,2,1
defer的使用场景:1.资源释放 (因为在退出前一定会执行defer)2.配合recover捕获异常,举例如下:
package main
import "fmt"
func main() {
defer func() {
if error := recover(); error != nil {
fmt.Println(error)
}
}()
a := 0
b := 1
fmt.Println(b / a)
}
defer的考点:通过defer修改返回值,考察输出,举例如下:
(1)defer虽然是延迟调用,但在调用之前,其参数就已经确定,因此后续对参数的修改不影响defer的输出,比如下面的输出应该是1
package main
import "fmt"
func main() {
num := 1
defer fmt.Println(num)
num = 2
return
}
(2)但是如果一开始指定的是地址,那就不一样了,因为此时不变的是地址,但地址中的内容是可能会变得,因此下面输出100,2,3,4,5
package main
import "fmt"
func main() {
nums := [5]int{1, 2, 3, 4, 5}
defer printarr(&nums)//此时传入的是地址,地址是不变的
nums[0] = 100//但是在结束前,地址中的内容变了
return
}
func printarr(arr *[5]int) {
for i := range arr {
fmt.Println(arr[i])
}
}
(3)结合return 有几种不同的情况:
package main
import "fmt"
func defer1() (res int) {
nums := 1
defer func() {
res++
}()
return nums
}
func main() {
fmt.Println(defer1())
}
这个例子返回2.因为return这种绑定的例子,可以分三步走,首先nums是1,接着return的时候,nums的值赋给了res,此时res为1.然后defer 此时res为2,接着返回真实的res
而下面这个,
package main
import "fmt"
func defer1() int {
nums := 1
defer func() {
nums++
}()
return 1
}
func main() {
fmt.Println(defer1())
}
返回的是1,因为这种无返回值参数的情况可以看作赋值给tmp变量,赋值后tmp为1,然后defer nums变成2,但是tmp没变,最后返回的是tmp,因此还是1 。
下面这种情况是一样的分析,也返回1,因为最后返回的是res,和nums没什么关系
package main
import "fmt"
func defer1() (res int) {
nums := 1
defer func() {
nums++
}()
return nums
}
func main() {
fmt.Println(defer1())
}
14.recover:Recover 用于捕获程序中的panic,使程序发生panic时不会终止,而是继续运行。
关于recover的位置,如果放在开头,可能后面有异常而无法捕获,放在结尾,中间有异常而无法捕获,因此配合defer放在开头(方便代码阅读)
defer会在函数退出的时候执行,不管是正常return 还是异常panic,具体示例如下:
package main
import "fmt"
func main() {
defer func() {
if error := recover(); error != nil {
fmt.Println("出现了panic,使用recover获取信息 ", error)
}
}()
fmt.Println(111)
panic("异常")
fmt.Println(222)
}
至于recover的调用顺序, 当函数发生panic,会优先呗当前函数的recover捕获,当前函数没有,则依次向外,如果main也没有,就退出。具体代码如下:
package main
import "fmt"
func testpanic1() {
fmt.Println("panic1的上半部分")
testpanic2()
fmt.Println("panic1的下半部分")
}
func testpanic2() {
defer func() {
recover()
}()
fmt.Println("panic2的上半部分")
testpanic3()
fmt.Println("panic3的下半部分")
}
func testpanic3() {
fmt.Println("panic3的上半部分")
panic("出现异常")
fmt.Println("panic3的下半部分")
}
func main() {
fmt.Println("程序开始")
testpanic1()
fmt.Println("程序结束")
}
testpanic运行到3发现异常,但是没有recover,因此向上抛出,2中有,捕获异常,因此1中的下半部分能够正常运行,而3挂掉了,所以2,3的下半部分都无法运行,但异常捕获完了,就可以继续运行到结束
15.依赖管理:用gomodule来做依赖管理,go version>go 1.13
GO111MODULE =OFF 禁用模块,查找依赖的时候在GOPATH下和VENDER文件夹,ON为启用,这时候会根据go.mod下载依赖,AUTO时,项目不在gopath/SRC目录下时,且项目有go.mod文件,则开启模块。
常见的命令有两条,go mod init 初始化文件夹为一个module项目,创建go.mod文件
go mod tidy 下载缺少的module,删除无用的module
在以下两个场景中使用go module 主要需要:
1.新项目中:cd 到主目录 执行go mod init 然后执行 go module tidy
2.修改项目依赖
修改go mod 然后执行go mod tidy
注意可以首先查看一下GO111MODULE 是否开启,命令为go env |grep(windows下为findstr) GO111MODULE
在执行完命令之后会生成一个go.sum文件,用来校验依赖包的正确性。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://ohhbene.com/golang%e7%9f%a5%e8%af%86%e6%80%bb%e7%bb%93.html