美文网首页
go 从0开始学习笔记【1】

go 从0开始学习笔记【1】

作者: 张清柏 | 来源:发表于2021-04-01 20:54 被阅读0次

    go面试题学习笔记
    go高级面试题
    go语言设计与实现
    go语言的深度理解-知乎
    [Golang数据类型](https://www.jianshu.com/p/ea17fa167585

    • 关于学习
      学习的方法可以说是一千个人有一千个哈姆雷特。我比较讨厌教科书式的学习,把书从第一页翻到最后一页,这样可能是表面上读完了。但是记住了多少呢?其实呢,我们可能往往只需要记住一个点就行了,一个点可以牵扯出很多东西,以点及线,以线及面
    • 安装略
    • 创建项目
    mkdir /Users/zhangguofu/app/goApp
    cd /Users/zhangguofu/app/goApp
    
    • 使用go mod 作为包管理
    go mod init gone
    
    • 遇到报错 $GOPATH/go.mod exists but should not,
      • 产生原因:开启模块支持后,并不能与GOPATH共存,所以把项目从$GOPATH中移出即可,在goland 中配置即可
    • 创建一个web服务,代码如下
    package main
    
    import (
        "net/http"
        
        "github.com/labstack/echo"
    )
    
    func main() {
        e := echo.New()
        e.GET("/", func(c echo.Context) error {
            return c.String(http.StatusOK, "Hello, World!")
        })
        e.Logger.Fatal(e.Start(":1323"))
    }
    
    
    image.png image.png
    • 可见 go mod 解决了包依赖的问题,会自动下载所需要的包

    怎么解决goland 报错问题呢?

    • 我们已经运行了项目,但是goland中的包并不能点击进入,怎么办呢?可以执行以下命令
    go mod tidy
    

    怎么解决包引用的问题?

    • 首先看我的目录


      image.png
    • 那么我这么命名我的module


      image.png
    • 我引用src/math下面的的math.go里面的方法,包名和文件名往往一样。怎么使用呢,看一下我的math.go文件,就是一个加法返回,很简单
    package math
    
    func Add(a int ,b int)int  {
        return a+b;
    }
    
    
    • 再看我main文件怎么调用吧
    package main
    
    import (
        "fmt"
        "goapp/src/math"//在module目录下寻找文件
    )
    
    func main() {
    
        sum:=math.Add(2,3)
        fmt.Println(sum)
    }
    
    

    https://zhuanlan.zhihu.com/p/109828249

    下面开始学习go 面试题,并总结经验

    首先聊一聊第一个话题 defer、panic、recover实践

    先来了解一下代表什么意思

    defer

    defer 语句将一个函数放入一个栈中,defer 会在当前函数返回前执行传入的函数,经常用于关闭文件描述符,数据库连接,redis连接等,用于清理资源,避免资源浪费。比如下面这个栗子

    package main
    
    import (
        "fmt"
        "goapp/src/math"
    )
    
    func main() {
    
        sum:=math.Add(2,3)
        fmt.Println(sum)
        defer func() {fmt.Println("i am defer1")}()
        res := test_defer();
        fmt.Println(res)
    
    }
    
    func test_defer() float64  {
        defer func() {fmt.Println("i am defer2")}()
        defer func() {fmt.Println("i am defer3")}()
        res :=math2.Mod(5,3)
        return res;
    }
    
    
    

    执行结果是什么呢?

    1. 执行 一个加法,打印返回值 5;

    2.defer1入栈

    3.执行函数test_defer,defer2入栈,defer3入栈,执行函数逻辑,在return 之前 呢,会执行 栈里面的defer,栈嘛,先进后出,和队列相反,所以依次执行defer3,defer2,然后返回结果

    4.main函数收到test_defer的返回值,开始打印结果

    5.main函数在结束之前呢,会执行一下本函数内的defer,所以开始执行defer1

    那结果是不是这样执行的呢?我们来看一下结果,毫无相差


    image.png
    那么此处可能有小伙伴要问一下,defer为什么要设计成栈?
    • 通俗来讲一个场景,defer 是做清场工作的,对吧,那么这样一个场景,一个小偷去仓库偷东西,干完活了,要擦除脚印对吧,那他不可能从进门的位置开始擦脚印吧,他只能退着擦,先擦最后一步的脚印,而且,很多时候最后一步是基于前面的基础上的,比如,还是这个小偷,他想偷柜子里面的珠宝,那他是不是得先打开门啊,那小偷做清理工作的时候,不可能先关闭门,在关闭柜子吧。
    • defer是用来释放资源的,比如一个操作,先给文件上锁,然后修改文件。那defer执行的时候应该是先关闭文件,再释放锁。如果释放锁,再关闭文件,那不是乱套了吗?从因果关系上说,后分配的资源可能会依赖前面的资源,但是前面的资源肯定不会依赖后面打开的资源。所以倒过来执行关闭 不会产生问题。
    那有的人说,我就是让defer先进先出,不行吗?允许是允许,但是不提倡。哈哈,是不是感受到了罗翔老师的气场,请看下面的代码,如果defer嵌套,那么defer会从外往里执行,剥洋葱似的,一层一层剥。
    package main
    
    func main() {
        defer func() {
            println("i am defer1")
            defer func() {
                println("i am defer2")
                defer func() {
                    println("i am defer3")
                }()
            }()
        }()
    
        panic("i am panic")
    }
    
    
    image.png

    panic

    panic往往和recover是成对出现的,与defer也有紧密的联系,panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine(协程)中递归执行调用方的defer

    我们看下面一段代码

    package main
    
    import "time"
    
    func main()  {
        defer func() {
            println("i am main defer1")
        }()
    
        go func() {
            defer func() {
                println("i am goroutine defer2")
            }()
    
            defer func() {
                println("i am goroutine defer3")
            }()
    
            panic("i am panic")
    
            defer func() {
                println("i am goroutine defer4")
            }()
        }()
        time.Sleep(1 * time.Second)
    }
    
    • 从前面的分析我们得知以下结果
    1. defer1 入栈

    2.执行goroutine
    3.defer2 入栈
    4.defer3入栈
    5.panic打断程序执行,依次执行defer3,defer2,panic,而panic 后面的程序不会再运行,并且main里面的defer也不会执行


    image.png
    • 我为什么要加time.Sleep 如果不加呢?


      image.png

    从截图里面看到,如果没有time.Sleep,协程好像没有被执行一样,为什么会这样呢?因为我们知道,协程不是抢占式的,如果删除time.Sleep,主goroutine不会放弃对辅助goroutine的控制,但是goroutine 必须放弃控制才能运行另一个goroutine,而time.Sleep就是放弃控制的一种方法。简单来说,你这个程序 从头到尾都是被main 主协程占用着,子协程不会主动抢占cpu,那么必须得是主协程主动让出cpu,让子协程有机会被cpu轮询到,子协程才会被执行

    详细讲一讲什么是协程

    • 协程是go语言最重要的特色之一,那么我们怎么理解协程呢?协程,简单说就是轻量级的线程,一个协程的大小是2k 左右,这也解释了为什么go能单机百万。
    • go语言里的协程创建很简单,在go关键词后面加一个函数调用就可以了。代码举栗
    package main
    
    import "time"
    
    func main()  {
        println("i am main goroutine")
        go func() {
            println("i am goroutine_in_1")
            go func() {
                println("i am goroutine_in_2")
                go func() {
                    println("i am goroutine_in_3")
                }()
            }()
        }()
    
        time.Sleep(1*time.Second);
        println("main goroutine is over")
    }
    
    
    image.png

    main 函数是怎么运行的呢?其实main函数也是运行在goroutine里面,不过是主协程,上面的栗子我们是嵌套了几个协程,但是他们中间并没有什么层级关系,协程只有两种,子协程和主协程。上面的代码中,我们让主协程休息了一秒,等待子协程返回结果。如果不让主协程休息一秒,即让出cpu,让子协程是没有机会执行的,因为主协程运行结束后,不管子协程是任何状态,都会全部消亡。

    但是在实际使用中,我们要保护好每一个子协程,确保他们安全运行,一个子协程的异常会传播到主协程,直接导致主协程挂掉,程序崩溃。比如下面这个栗子

    package main
    
    import "time"
    
    func main()  {
        println("i am main goroutine")
        go func() {
            println("i am goroutine_in_1")
            go func() {
                println("i am goroutine_in_2")
                go func() {
                    println("i am goroutine_in_3")
                    panic("i am panic")
                }()
            }()
        }()
    
        time.Sleep(1*time.Second);
        println("main goroutine is over")
    }
    
    
    image.png

    最后一句,main goroutine is over没有打印,程序没有执行到。 前面我们说到了。不管你是什么样的程序,遇到panic 我就终止程序往下执行,哪怕是子协程呢!好了,协程先说到这里。我们继续往下看recover

    recover

    recover 可以中止panic造成的程序崩溃,它是一个只能在defer中发挥作用的函数。在其他作用域中不会发挥作用。为什么这么说呢?我们看下面这个栗子

    package main
    
    import "fmt"
    
    func main() {
        defer func() {
            println("i am main")
        }()
        if err := recover();err != nil {
            fmt.Println(err)
        }
        panic("i am panic")
    }
    
    
    

    看一下执行结果


    image.png

    我们看到,遇到panic,执行了defer,然后执行了panic ,并没有执行if条件判断,为什么?recover是捕捉错误的,运行到if 还没有错误,捕捉什么?运行到panic 的时候 if 已经执行过了,怎么捕捉?那么可能有人想,我把if放到panic后面不就行了吗?行吗?答案是否定的,panic 我们前面已经说过了,甭管你是谁,看见我就得停止。那就回到我们刚才说的,panic 出现,程序停止往下执行,但是程序会循环执行defer啊,那如果我在defer里面捕捉错误,是不是就可以解决这个问题了呢。可见go的设计者是用心良苦!到这里有没有人会问一个问题defer可以嵌套,那么panic能否嵌套呢?当然可以,defer可以容纳一切,panic放到defer里面一样可以嵌套

    package main
    
    func main() {
        defer func() {
            defer func() {
                defer func() {
                    panic("i am panic3")
                }()
                panic("i am panic2")
            }()
            panic("i am panic1")
        }()
    
        panic("i am panic")
    }
    
    image.png

    为什么会先执行 最后一行panic ,才执行defer呢,这和前面说的遇到panic先执行defer有点出入是吧,但是你这样看 defer优先于panic优先于defer+panic。

    那么现在,我们来写一个例子,看defer 如何捕捉panic并恢复程序正常执行

    package main
    
    import "fmt"
    
    func main() {
        havePanic();
        println("i will go on ")
    }
    
    func havePanic()  {
        defer func() {
            if err:=recover();err !=nil {
                fmt.Println("i get a panic")
                fmt.Println(err)
            }
        }()
        panic("i am panic");
    }
    
    

    解读一下上面的程序,执行havePanic ,havePanic的第一个defer入栈,往下执行碰到panic,首先会执行defer,defer里面打印了err信息,并可以做一些其他的处理,比如记录日志,重试什么的。然后main继续执行下面的print,看一下执行结果


    image.png

    下面再补充一点协程的知识

    go不是号称百万协程吗?那么我们真给它来个百万协程看一下我的电脑到底能不能hold住

    来!写一段代码

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main()  {
        i :=1;
        for  {
            go func() {
                for  {
                    time.Sleep(time.Second)
                }
            }()
            if i > 1000000 {
                fmt.Printf("我已经启动了%d个协程\n",i)
            }else{
                fmt.Printf("当前是第%d个协程\n",i)
            }
            i++
        }
    
    }
    
    

    截图看一下我当前的机器状态


    image.png

    百万协程挂起之后的截图


    image.png

    因为输出跟不上速度其实最后跑了1842504个协程


    image.png

    说一下跑后感:风扇呼呼的转了大概三分钟的样子
    ,我算了一下一个协程大概是2.45kb的样子


    image.png

    协程和线程的区别

    一个进程内部可以运行多个线程,而每个线程又可以运行很多协程。线程要负责对协程进行调度,保证每个协程都有机会得到执行。当一个协程睡眠时,它要将线程的运行权让给其它的协程来运行,而不能持续霸占这个线程。同一个线程内部最多只会有一个协程正在运行。

    线程的调度是由操作系统负责的,调度算法运行在内核态,而协程的调用是由 Go 语言的运行时负责的,调度算法运行在用户态。

    协程可以简化为三个状态,运行态、就绪态和休眠态。同一个线程中最多只会存在一个处于运行态的协程,就绪态的协程是指那些具备了运行能力但是还没有得到运行机会的协程,它们随时会被调度到运行态,休眠态的协程还不具备运行能力,它们是在等待某些条件的发生,比如 IO 操作的完成、睡眠时间的结束等。

    操作系统对线程的调度是抢占式的,也就是说单个线程的死循环不会影响其它线程的执行,每个线程的连续运行受到时间片的限制。

    Go 语言运行时对协程的调度并不是抢占式的。如果单个协程通过死循环霸占了线程的执行权,那这个线程就没有机会去运行其它协程了,你可以说这个线程假死了。不过一个进程内部往往有多个线程,假死了一个线程没事,全部假死了才会导致整个进程卡死。

    每个线程都会包含多个就绪态的协程形成了一个就绪队列,如果这个线程因为某个别协程死循环导致假死,那这个队列上所有的就绪态协程是不是就没有机会得到运行了呢?Go 语言运行时调度器采用了 work-stealing 算法,当某个线程空闲时,也就是该线程上所有的协程都在休眠(或者一个协程都没有),它就会去其它线程的就绪队列上去偷一些协程来运行。也就是说这些线程会主动找活干,在正常情况下,运行时会尽量平均分配工作任务。

    我的线程数到底有多少?

    默认情况下,Go 运行时会将线程数会被设置为机器 CPU 逻辑核心数。同时它内置的 runtime 包提供了 GOMAXPROCS(n int) 函数允许我们动态调整线程数,注意这个函数名字是全大写。该函数会返回修改前的线程数,如果参数 n <=0 ,就不会产生修改效果,等价于读操作。

    package main
    
    import (
        "fmt"
        "runtime"
    )
    
    func main()  {
        fmt.Print(runtime.GOMAXPROCS(0))//获取默认线程数 8
        println("\n")
        runtime.GOMAXPROCS(10)//设置线程数为10
        fmt.Print(runtime.GOMAXPROCS(0))//获取新线程数 10
    }
    
    
    image.png

    今天学习一下slice的使用方法

    我们先来看一道面试题来引出今天的问题

    package main
    
    import "fmt"
    
    func main()  {
        s1 :=[]int{1,2,3,4}
        m := make(map[int]*int)
        for key,val :=range s1{
            m[key]=&val;
        }
    
        for key,val := range m{
            fmt.Println(key,"->",*val)
        }
    }
    

    打印结果


    image.png

    先来看一下go的语言结构

    go的语言基础部分由这几个部分构成,其实其他语言也都差不多,我以php对比

    • 包声明
      就像命名空间
    • 引入包
      就是命名空间下面的use
    • 函数
      函数的命名 分为首字母是否大写,大写的话可以在包外调用,小写的话就自己玩自己了。这个很好解释,小名,比如 狗剩儿,小花儿,在自己的圈子里叫叫就得了,对外还得使用大名,在一个go文件中,包名+函数名 构成唯一性,那如果不唯一 怎么办?编译直接就报错了,报错内容function redeclared in this block,php里面是命名空间+函数名 来解决函数名冲突的问题,对吧。那个 包外是否能否访问 有点像 方法属性,小写的是私有属性,pravite,大写的就是public
    • 变量
      首先我们先声明这一点,Go语言通过首字母的大小写来控制访问权限。 无论是方法,变量,常量或是自定义的变量类型,如果首字母大写,则可以被外部包访问,反之则不可以,包括结构体也是一样。

    我们知道,go是静态语言,因此go里面的变量肯定是要明确类型的,那么有几种定义变量的方式呢
    1.var name type ,var 是声明变量的关键字,name 是变量名,type 是变量的类型,比如

        var age int
        age =12
        println(age)
    

    但是这里有一个小问题,为什么是 var name type ,而不是 var type name?

    第一种解释是这样:语义,读起来通顺,比如英文讲,vary age as int 以整形的类型定义age;

    而第二种解释 是从编译的角度来讲的,我们先来看go的声明语法结构,完整的声明应该是如下这种

    var age  int = 12  <==> (var) + (age) + [int] + [ = 12]
    

    发现了什么没有?了解正则的朋友应该明白()里面都是必填的,[]是选填的。所以go其实就是 把必填的都放到前面,把可以缺省的放到后面,这样是符合编译原理的,确保编译器parse这结构的时候是正确的,比如,go里面可以这样

    package main
    
    func main()  {
        var age,score ,score1 int
        age =12
        score=100
        score1=101
        println(age,score)
    }
    
    

    如果你不想var 太多,也可以这样

        var (
            a int
            b int
            c bool
            d float64
        )
    

    在函数的内部呢,可以更简单的使用 :=来完成赋值操作

    func main()  {
        a:=12
        b,c:="abc",123
        println(a,b,c)
    
    }
    

    但是这样命名要注意以下几个问题
    1.只能用在函数的内部
    2.必须要显式的初始化,什么是显式?我们前面说过,go是静态语言,任何变量都要声明类型,如果你不声明,那么至少应该让编译器通过推断的方式知道你是什么类型,比如a:=12,我可以推断出来a 是整形的,
    3.如果你想直接的提供数据类型,就像 var一样,带出数据类型,行不行呢?不行,这就是第三个限制,不能提供数据类型,也就是编译器都可以推断出来了,你就别画蛇添足了,另外一方面,在编译时推断变量类型的语言,还有c++;

    • 语句表达式
    • 注释

    go 变量的声明方法

    在前面,go 声明 变量,比如 字符串,数字,bool等简单类型的已经说过了,那么还有一种复合类型,我们在这里讲一讲

    指针类型

    指针其实就是指向一个对象的地址值,对指针的任何操作都会映射到指针所指的对象上面,而这个对象可以是任何一种数据类型,包括指针自己。我们看下面这个栗子

    package main
    
    func main()  {
    
        var b * int
        a:=20
        b=&a
        c :=&a
        println(b,c)
    }
    
    
    

    指针类型既可以var来实现,也可以 :=来赋值,但是我们来看,var b * int,为什么会有一个呢?代表的是b的类型 指针,而int 代表的是b所指向的对象是int,还是那句话,静态语言任何变量都有类型,包括指针。

    但是我们在这段代码里面,包括前面的代码,平凡看到*和&的出现,那么他们两个到底什么关系呢?那我们来探讨一下

    *和&的关系

    专业的说,&是取地址符号,获取某个对象的地址,比如上面我们用 &a获取到了a的地址并赋值给了指针b,

    • 首先表示是一种指针类型,对吧,我们定义b 的时候,是这样var b * int,这样定义了b是指针类型,但是我们想一下,我们既然拿到了指向对象的地址,就是&a,那么我们怎么知道 a的值呢?这就是*的第二个作用,不仅用来作为指针存储指向对象的地址,我还可以根据地址获取到对应的值。怎么获取值呢?我们看下面代码
    package main
    
    func main()  {
    
        var b * int
        a:=20
        b=&a
        c :=&a
        println(b,*c)
    }
    
    
    

    看一下输出


    image.png

    数组类型的声明方法

    我们前面看见过这个

    var age  int = 12  <==> (var) + (age) + [int] + [ = 12]
    

    数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

    但是对于复合类型的数据,我们应该怎么声明呢,我们先看一下数组
    var <数组名称> = [<数组长度>]<数组元素类型>{元素1,元素2,...},可以看到,中括号是可以缺省的,元素也是可以缺省的,应该默认填充零值,所谓零值,看不同的元素类型,比如int的零值是0,字符串的零值就是空字符串,比如下面这个

    package main
    
    func main()  {
        var arr=[5]int{1,2,3,4}
        for key,val:=range arr{
            println(key,val)
        }
    }
    
    
    

    或者你也可以这样

    package main
    
    func main()  {
        var arr=[]int{2,3,4,5,6}
        for key,val:=range arr{
            println(key,val)
        }
    }
    
    

    声明时如果不设定大小,赋值后语言本身会计算数组大小,如果在里面想指定某个元素的索引呢,用冒号就可以了 比如以下

    package main
    
    func main()  {
        var arr=[...]int{8:8,3:3,4,5,6}
        for key,val:=range arr{
            println(key,val)
        }
    }
    //打印效果如下
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    0 0
    1 0
    2 0
    3 3
    4 4
    5 5
    6 6
    7 0
    8 8
    

    slice类型的声明方法

    slice 切片是对数组的抽象,由于数组的长度不能改变,这就导致数组的灵活性不高,所以go提供了一种动态数组,这就是切片,虽然是动态数组,但是本质还是有区别的

    • 数组是值类型,值类型是相对于引用类型来讲的,比如以下代码
    func main()  {
        var arr=[3]int{1,2,3}
        var b=arr
        println(&arr,"------------\n")
        println(&b,"------------\n")
    
        println("------------\n")
        b[1]=66
        for key,val:=range arr{
            println(key,val)
        }
        for key,val:=range b{
            println(key,val)
        }
    }
    
    //输出
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    0xc00003a748 ------------
    
    0xc00003a730 ------------
    
    0 1
    1 2
    2 3
    ------------
    
    0 1
    1 66
    2 3
    
    Process finished with exit code 0
    
    

    你把一个数组的值赋值给另一个数组,其实是另开了一块内存

    • slice 是一个引用类型,一个动态的执行数组切片的指针,而且是一个被封装过的指针
    • slice 是一个不定长的,总是指向底层的数组
      我们来看一下slice是否是引用值
    func main()  {
        var arr=[]int{1,2,3}
        var b=arr
        println(&arr,"------------\n")
        println(&b,"------------\n")
    
        println("------------\n")
        b[1]=66
        for key,val:=range arr{
            println(key,val)
        }
        for key,val:=range b{
            println(key,val)
        }
    }
    //输出
    0xc00003a760 ------------//这是slice arr的地址
    
    0xc00003a748 ------------//这是slice b的地址
    
    ------------
    
    0 1
    1 66
    2 3
    0 1
    1 66
    2 3
    
    
    • slice的另一种声明方法
    func main()  {
        b:=make([]int,5,10)//元素个数为5,即size,预留10个元素的空间
        println(cap(b),"\n")
        println(len(b),"\n")
        for key,val:=range b{
            println(key,val)
        }
    }
    //输出
    10 
    
    5 
    
    0 0
    1 0
    2 0
    3 0
    4 0
    

    有没有感觉到 b :=[]int 特别像我们之前写的数组arr :=[5]int{},没有强调中介的长度和后面的元素

    map类型的声明方法

    我们再来看 一下什么是map,map 是一种无序的键值对集合,通过key可以快速检索到value,我们可以像数组一样迭代它,但是我们并不能决定map的返回顺序,这是因为 Map 是使用 hash 表来实现的。与 slice 类似也是一个引用类型。map 本身其实是个指针,指向内存中的某个空间。声明方式与数组类似,声明方式:

    • var 变量名 map[key类型]值类型 或直接使用 make 函数初始化:
    • make(map[key类型]值类型, 初始空间大小)
    • 其中key值可以是任何可以用==判断的值类型,对应的值类型没有要求。
    • 声明之后需要初始化使用,声明后赋值报错,make 就是 返回空值
    func main()  {
        //第一种形式
        //var m1 =map[string]string{}
        //m1 :=map[string]string{}
        m1 :=make(map[string]string);
        m1["name"]="xiaoqiang"
        m1["age"]="20"
        fmt.Println(m1)
    }
    
    • 接下来我们看一下单引号和双引号的区别
      在Go中,双引号是用来表示字符串string,其实质是一个byte类型的数组,单引号表示rune类型。rune代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
    func main()  {
    
        s1:="abcde"
        s2:='8'
        println(s1[1:3])
        println(s2)
    }
    
    //输出
    bc
    56
    
    Process finished with exit code 0
    
    
    image.png
    • make 和new的区别

    make 和new 都在堆内存分配空间,但是他们的行为和适用的类型有所不同;
    new只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时请注意它同时把分配的内存置为零,也就是类型的零值。它返回的永远是类型的指针,指向分配类型的内存地址。以下代码是等价的

    package main
    
    import "fmt"
    
    func main()  {
    
        p1 := new(int)
        fmt.Println(p1)
        fmt.Println(*p1)
    
        var p2 * int
        i:=0
        p2=&i
        fmt.Println(p2)
        fmt.Println(*p2)
    }
    

    new 返回的指针,指针指向的就是该类型的0值

    make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建,而且可以接收多个参数,比如b:=make([]int,5,10)//元素个数为5,即size,预留10个元素的空间,有没有想到有点像php里面构造函数,make(T, args) 返回的是初始化之后的 T 类型的值,这个新值并不是 T 类型的零值,也不是指针 *T,是经过初始化之后的 T 的引用。比如我们上面举栗,b是一个slice,length和cap都已经被初始化完成,以零值填充。

    package main
    
    import "fmt"
    
    func main()  {
    
    s1:= make([]int,5,10)
    println(len(s1),"\n")
    println(cap(s1),"\n")
    fmt.Printf("%#v",s1)
    }
    //输出
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    5 
    
    10 
    
    []int{0, 0, 0, 0, 0}
    Process finished with exit code 0
    
    

    channel

    channel 为什么出现?其实channel 就是为了配合go的goroutine使用的,为协程直接的通讯提供通道,前几天看一本书提到,goroutine本来是互相独立的,像一个一个的小岛,而channel就是连接这些小岛的桥梁,让goroutine直接可以互相通信

    和map一样,声明之后需要初始化使用,声明后赋值报错,make 就是 返回空值

    package main
    
    import "time"
    
    func main()  {
        var c1 chan int
        c1=make(chan int)
    
        go func() {
            c1<-1
            c1<-2
            c1<-3
            c1<-4
        }()
    
        v1:=<-c1
        v2:=<-c1
        println(v1,v2)
        time.Sleep(1*time.Second)
    }
    //输出
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    1 2
    
    Process finished with exit code 0
    

    chanle 是先进先出的,这个以后会具体介绍用法

    struct

    结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。有点像我们phper常用的orm里面的定义,把它当对象使用就行了


    image.png

    比如我们现在定义一个person,先来看一下 声明方法,这个和interface和error 都一样 ,这里再次提示一下,注意大小写,小写包外访问不到呢!

    package main
    
    func main()  {
    
        p1 := person{"技术小虫",20}
        println(p1.name)
    }
    
    type person struct {
        name string
        age int
    }
    
    //输出
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    技术小虫
    
    
    

    interface

    我们还是先问一下,为什么会有interface?我们知道,在go里面没有对象和类的概念,那么如果我想赋值一组功能给某个结构体,怎么办呢?比如说,人,有吃喝拉撒的功能对吧,但是每个人吃点 还不一样,我不能一个一个写吧,那怎么办嗯?这个时候,interface的功能就提现出来了

    package main
    
    import "strconv"
    
    func main() {
        p1 := person{"技术小虫", 20}
        res :=p1.eat("hello,")
        res2 :=p1.say("hello,")
        println(res,"\n",res2)
    }
    
    type person struct {
        name string
        age  int
    }
    
    type behavor interface {
        eat(s string) string
        say(s string) string
    }
    
    func (p1 person) eat(s string)string {
        return s + "i am " + p1.name
    }
    
    func (p1 person)say(s string )string  {
            return s + "i am " + strconv.Itoa(p1.age)
    
    }
    
    //输出
    /private/var/folders/z5/b9w5zj9s4n322m0pzt95hwf40000gn/T/___go_build_main_go
    hello,i am 技术小虫 
     hello,i am 20
    
    Process finished with exit code 0
    
    
    

    Error的自定义方法和使用

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func main() {
        e1 :=errors.New("i am an error")
        fmt.Println(e1.Error())
        e2:= fmt.Errorf("i am an another error")
        fmt.Println("\n",e2)
    }
    
    
    • 好了,go的常用一些 类型已经介绍完毕了,下期我们会继续一点带线,以线带面的方式来学习!

    相关文章

      网友评论

          本文标题:go 从0开始学习笔记【1】

          本文链接:https://www.haomeiwen.com/subject/ooojhltx.html