美文网首页
go 学习笔记

go 学习笔记

作者: 天空蓝雨 | 来源:发表于2020-06-28 09:13 被阅读0次

    这篇文章就总结一下go 的细节
    具体参考:
    http://c.biancheng.net/golang/
    但是里面有写 收费章节(其实他也是抄的别的,暂时没找到源头)

    这个是 gitbook 的文档,但是好像有点岁月的痕迹
    https://wizardforcel.gitbooks.io/gopl-zh/preface.html
    这也是gitbook 文档,稍微美观一点的
    https://learnku.com/docs/the-way-to-go/an-actual-example-of-128-using-the-interface-fmtfprintf/3668
    Go接口类型的使用

    • 数组声明
    var 数组变量名 [元素数量]Type
    

    数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值

    func get_array_length() int {
            return 3
    }
    
    func main() {
        length := get_array_length()  
        fmt.Println(length)  
        var a [len("sffddd")] int        // 定义三个整数的数组   但是内置
    函数 比如 len 就不会报错
        // var a [get_array_length()]int   // 数组长度必须在编译的时候
    就搞定,否
    则编译报错   这个就包错:non-constant array bound length  
        // 现在算是理解了一点,解释语言和编译语言的区别,编译语
    言关于内存
    分配的初始化都要,在编译的时候就要搞定, 
        b := make([]int, get_array_length())   // 这种作为函数参数的
    还是可以 使用
        fmt.Printf("%v\n", b)
        fmt.Println(a[0])        // 打印第一个元素
        fmt.Println(a[len(a)-1]) // 打印最后一个元素
    }
    

    现在算是理解了一点,解释语言和编译语言的区别,编译语言关于内存分配的初始化都要在编译的时候就要搞定,不能间接间接计算获得(除了一些内置函数,如上面的len) 这个以后要多练习,才能理解了

    • go 多维数组

    记住,go 数组是 值类型, 多维数组也是会按照 类型的初始值来填充的

    var array_name [size1][size2]...[sizen] array_type
    

    从前到后的 size 分别是外层到内层的

    // 声明一个二维整型数组,两个维度的长度分别是 4 和 2
        var array [4][2]int
        array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
        // 写了索引就按照索引赋值,默认是按照顺序的
        array = [4][2]int{1: {20, 21}, {40, 41}}   
    // [[0 0] [20 21] [40 41] [0 0]]
        array = [4][2]int{1: {20, 21}, 3:{40, 41}}  
    // [[0 0] [20 21] [0 0] [40 41]]
    

    如果指定了索引,初始化,后面不指定索引的话,就按照前面的索引+1 来赋值了

    • 切片(slice)

    是对数组的一个连续片段的引用,所以切片是一个引用类型

    a := [3] int {1,2,3}
    b:= a[:]
    a[0] = 10
    fmt.Println(b)  // [10 2 3]
    数组直接切片就是返回的就是切片类型, 他是原来数组的一个引
    用,如果原来数组改变,切片的值也是跟着变的
    

    而且切片是引用类型,索引当做函数参数,在函数内部的改变也是会影响外面的值的。

    一般 [ num ] 或者 [...] 这种 有长度的是数组 ,而没有长度的是 切片
    切片可以从数组或切片生成新的切片, 他们都是原来对象的引用

    切片复位

    a := []int{1, 2, 3}
    slice[0:0]  // 两者同时为 0 时,等效于空切片
    

    切片分配地址
    声明一个切片的时候:
    a := [] int // 此时a 为 nil 0x0

    a := [] int {} // 此时 a 为 [] 但不是 nil 已经分配了地址
    a := make([] int , length, cap) // a 为 [ ] 也已经分配了地址
    使用 make 的好处是 可以指定 切片初始 的大小和容量

    (使用make 一定分配了内存,但是直接 [a:b] 只是把切片指向了已经存在的内存区域, 而没有重新分配内存)

    • 切片 append()
      append 给切片动态的添加元素
      尾部追加
    var a []int
    a = append(a, 1) // 追加1个元素
    a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
    a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
    

    开头添加(一般导致内存重新分配,已有元素复制一份)

    var a = []int{1,2,3}
    a = append([]int{0}, a...) // 在开头添加1个元素
    a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
    

    append 原地址空间不足就会扩容 和 py 类似都是 二倍空间 递增

    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), 
    cap(numbers), numbers)
    }
    
    > len: 1  cap: 1 pointer: 0xc00004a080
    len: 2  cap: 2 pointer: 0xc00004a0c0
    len: 3  cap: 4 pointer: 0xc0000480e0
    len: 4  cap: 4 pointer: 0xc000040e0
    len: 5  cap: 8 pointer: 0xc000078080
    len: 6  cap: 8 pointer: 0xc000078080
    len: 7  cap: 8 pointer: 0xc000078080
    len: 8  cap: 8 pointer: 0xc000078080
    len: 9  cap: 16 pointer: 0xc00007a080
    len: 10  cap: 16 pointer: 0xc00007a080
    
    

    append 链式调用

    var a []int
    a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
    a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
    

    每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中

    切片扩容之后,会更换内存地址,这样就和原来的没关系了:

    a := [3] int {1,2,3}
    b:= a[:]  
    b = append(b, 5,6,7)
    b[0] = 10  // a  [1 2 3]  b [10 2 3 5 6 7]
    
    

    所以扩容更换地址,就不是引用原来对象了,而是直接复制了一个 新的。

    a := make([] int, 3, 5)
    a = []int{1,2,3}    //注意,这里  cap(a)=3并且和一开始的make分配的地址不同了,
    因为这里直接把a指向了另一个切片地址了
    b := append(a, 4)
    a[0] = 10
    fmt.Printf("b 是 %v, a 是 %v \n", b, a) 
    fmt.Printf("b 地址是 %p, a 地址是 %p \n", b, a)
    >>
    b 是 [1 2 3 4], a 是 [10 2 3] 
    b 地址是 0xc000070060, a 地址是 0xc0000480c0
    

    (发现 新变量接受append 返回值,都是新的地址(旧变量不扩容,就还是原来的地址)) 这个结论是错的哦
    无论用那个变量接受append,地址都是取决于有没有扩容 没有的话还是原来的地址。

    • copy() 复制切片

    copy( destSlice, srcSlice []T) int
    // 将 srcSlice 复制到 destSlice
    

    copy 函数直接操作 原切片 并返回替换的元素个数

    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{5, 4, 3}
    copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
    > slice2  //  [1,2,3]
    copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
    > slice1 // [1 2 3 4 5]
    

    append 返回新的切片 取决于地址是不是改变(是不是扩容了), copy 直接在目的切片直接修改, 复制 第二个参数切片。(返回替换的元素数量)

    • Go语言从切片中删除元素

    这个比较有意思
    开头删除
    普通做法(移动数据指针)

    a = []int{1, 2, 3}
    a = a[1:] // 删除开头1个元素
    a = a[N:] // 删除开头N个元素
    

    append实现 (后面的数据向开头移动, 不会导致内存空间结构的变化)

    a = []int{1, 2, 3}
    a = append(a[:0], a[1:]...) // 删除开头1个元素
    a = append(a[:0], a[N:]...) // 删除开头N个元素
    

    copy 实现

    a = []int{1, 2, 3}
    a = a[:copy(a, a[1:])] // 删除开头1个元素
    a = a[:copy(a, a[N:])] // 删除开头N个元素
    

    中间删除(对剩余的元素进行一次整体挪动)

    a = []int{1, 2, 3, ...}
    a = append(a[:i], a[i+1:]...) // 删除中间1个元素
    a = append(a[:i], a[i+N:]...) // 删除中间N个元素
    a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
    a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
    
    

    尾部删除

    a = []int{1, 2, 3}
    a = a[:len(a)-1] // 删除尾部1个元素
    a = a[:len(a)-N] // 删除尾部N个元素
    

    Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来

    • map

    map 是引用类型:

    var mapname map[keytype]valuetype
    

    [keytype] 和 valuetype 之间允许有空格
    使用和 slice 差不多
    声明 可以直接初始化,如果只是声明,那就要用make 来分配地址

    map 容量:

    make(map[keytype]valuetype, cap)
    

    如果提前知道map 大小,最好写一下 cap, 优化性能

    map 遍历:
    使用 for range 即可
    只使用 值

    for _, v := range scene {}
    

    只是用 key

    for k := range scene { }
    

    同时 key 和值

    for k, v := range scene { }
    

    map元素的删除和清空
    删除某个 key

    delete(map, 键)
    

    清空 map
    没有清空map 的函数,还是重新 make分配内存吧

    不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多
    sync.Map
    在并发环境中使用的map
    pass

    • list(列表)

    列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作
    这个以前没看过,需要看一下吧
    这个是要是 list 包的使用
    列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型
    初始化列表:

    变量名 := list.New()
    或:
    var 变量名 list.List
    

    大概使用 参考:
    http://c.biancheng.net/view/35.html

    • nil:空值/零值

    指针、切片、映射、通道、函数和接口的零值则是 nil
    数类型为 0
    字符串为 ""
    布尔为 false

    nil 不是关键字或保留字, 你可以 var nil = xx(但是不规范)

    两个为 nil 的值不能使用 == 比较, 只能单独和 nil 比较

    nil 大小不同,默认是当前类型的大小

    • go 流程控制

    pass

    • go 函数

    挑几个重点讲一下吧

    1、go 闭包
    函数 + 引用环境 = 闭包
    闭包就和 py 一样的道理,直接返回的函数,可以引用到他上级的东西。
    闭包具有记忆性,他可以记得外面的东西(会跟随闭包生命期一直存在)

    func Accumulate(value int) func() int {
        // 返回一个闭包
        return func() int {
            // 累加
            value++
            // 返回一个累加值
            return value
        }
    }
    
    func main() {
        accumulator := Accumulate(1)
        // 累加1并打印
        fmt.Println(accumulator())  // 2
        fmt.Println(accumulator())  // 3
        // 打印累加器的函数地址
        fmt.Printf("%p\n", &accumulator)  // 0xc000072018 这里用 &取闭包实例变量的地址 
        // 创建一个累加器, 初始值为1
        accumulator2 := Accumulate(10)  // 11
        // 累加1并打印
        fmt.Println(accumulator2())
        // 打印累加器的函数地址
        fmt.Printf("%p\n", &accumulator2) //0xc000072028
    
        // (不加&的话是去闭包函数的地址,所以下面结果是一样的)
        fmt.Printf("%p\n%p\n", accumulator, accumulator2)  // 0x498030  0x490830
    }
    

    2、可变参数
    这 py 可变参数一样的,只不过 go * 表示指针,所以用 ... 来表示 可变参数
    同样 args ...type 也必是函数最后一个参数才可以,是一个语法糖,在函数内部args 已经封装为 一个 切片了(py 里面是 封装成元祖)

    声明参数:

    args ...type
    

    type 表示 args 的参数只能是 type 类型的。如果你想支持任意类型,请把type 改为 interface{}

    args ...interface{}  // 这样 args  就可以是任意类型了 
    

    interface{} 传递任意类型数据是Go语言的惯例用法
    举个例子:

    func MyPrintf(args ...interface{}) {
        for _, arg := range args {
            switch arg.(type) {  // 这里用 Go 类型断言 .(type) 取出类型
                case int:
                    fmt.Println(arg, "is an int value.")
                case string:
                    fmt.Println(arg, "is a string value.")
                case int64:
                    fmt.Println(arg, "is an int64 value.")
                default:
                    fmt.Println(arg, "is an unknown type.")
            }
        }
    }
    func main(){
        var v1 int = 1
        var v2 int64 = 234
        var v3 string = "hello"
        var v4 float32 = 1.234
        MyPrintf(v1, v2, v3, v4)
    
    }
    

    上面用到了,Go 类型断言 参考:
    https://studygolang.com/articles/12355

    3、传递可变参数
    就是和py 也类似的, 这里使用 args ... 解包
    比如:

    append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
    等价与
    append(a, 1,2,3)
    

    4、Go语言defer(延迟执行语句)

    多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
    也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行
    类似 finally 语句

    func main() {
        fmt.Println("defer begin")
        // 将defer放入延迟调用栈
        defer fmt.Println(1)
        defer fmt.Println(2)
        // 最后一个放入, 位于栈顶, 最先调用
        defer fmt.Println(3)
        fmt.Println("defer end")
    }
    >> 
    defer begin
    defer end
    3
    2
    1
    

    注意 defer return 和返回值的顺序问题。
    如果是 没返回值 名的声明,那么 defer 不会改变 return 的值(指针除外),有返回值名,那就可能会改变 return 的值
    return xx > defer xxx >函数结束
    5、递归
    斐波那契数列

    package main
    import "fmt"
    func main() {
        result := 0
        for i := 1; i <= 10; i++ {
            result = fibonacci(i)
            fmt.Printf("fibonacci(%d) is: %d\n", i, result)
        }
    }
    func fibonacci(n int) (res int) {   // 指定返回的变量名,可以不用return了
        if n <= 2 {
            res = 1
        } else {
            res = fibonacci(n-1) + fibonacci(n-2)
        }
        return   // 返回值已经制定了,所以这里直接return ,后面不需要写参数了,当然写也没问题
    }
    

    6、Go语言处理运行时错误
    go 语言中错误处理思想在于,对于可能出现错误的函数,需要在返回值加一个 错误的error 类型,如果是成功的 error 值为 nil, 失败则返回具体的错误信息。(当然也可以,用defer 等关键字间接在函数内部实现错误处理,但是这种会增加程序复杂度)
    自定义一个错误
    使用 errors 包来声明一个错误类型(这是最基本的错误字符串)

    var err = errors.New("this is an error")
    

    简单除零错误

    import (
        "errors"
        "fmt"
    )
    // 定义除数为0的错误
    var errDivisionByZero = errors.New("傻逼错误,666")
    func div(dividend, divisor int) (int, error) {
        // 判断除数为0的情况并返回
        if divisor == 0 {
            return 0, errDivisionByZero
        }
        // 正常计算,返回空错误
        return dividend / divisor, nil
    }
    func main() {
        fmt.Println(div(1, 0))
    }
    
    >>
    0 傻逼错误,666
    

    自定义错误类型:
    (这个同 py Exception 定义自己的错误类型)

    package main
    import (
        "fmt"
    )
    // 声明一个解析错误
    type ParseError struct {
        Filename string // 文件名
        Line     int    // 行号
    }
    // 实现error接口,返回错误描述
    func (e *ParseError) Error() string {
        return fmt.Sprintf("%s:%d", e.Filename, e.Line)
    }
    // 创建一些解析错误
    func newParseError(filename string, line int) error {
        return &ParseError{filename, line}
    }
    func main() {
        var e error
        // 创建一个错误实例,包含文件名和行号
        e = newParseError("main.go", 1)
        // 通过error接口查看错误描述
        fmt.Println(e.Error())
        // 根据错误接口具体的类型,获取详细错误信息
        switch detail := e.(type) {
        case *ParseError: // 这是一个解析错误
            fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
        default: // 其他类型的错误
            fmt.Println("other error")
        }
    }
    

    这个暂时不太懂啊
    7、Go语言宕机(panic)
    (就是类似 py raise )
    最简单的使用:

    package main
    func main() {
        panic("主动泡出错误")
    }
    >>
    panic: 主动抛出错误
    routine 1 [running]:
    main.panic_error(...)...
    
    

    panic 内置函数的定义:

    func panic(v interface{})    //panic() 的参数可以是任意类型的。
    

    所以 panic 参数不止是字符串
    使用 数组参数:

    panic([3]int {1,2,3} )
    >>panic: ([3]int) (0x48d460,0xc0000480c0)
    

    上面说了defer 语句的作用,就是为了做一些 延迟处理,这里配上 panic 会非常好用

    package main
    import "fmt"
    func main() {
        defer fmt.Println("宕机后要做的事情1")
        defer fmt.Println("宕机后要做的事情2")
        panic("宕机")
    }
    >>
    宕机后要做的事情2
    宕机后要做的事情1
    panic: 宕机
    

    当执行 panic 的时候,后面的语句就不会执行了,但是panic 之前的语句会被执行,如果需要延迟处理的使用 defer 即可(其实感觉和正常执行差不多,无非 defer 顺序是先进后出的)

    8、宕机恢复(recover)
    recover 就是类似 py try :except 结构
    让程序在出现异常的时候,能够继续执行

    type panicContext struct {
        function string // 所在函数
    }
    // 保护方式允许一个函数
    func ProtectRun(entry func()) {
        // 延迟处理的函数
        defer func() {
            // 发生宕机时,获取panic传递的上下文并打印
            err := recover()
            switch err.(type) {
            case runtime.Error: // 运行时错误
                fmt.Println("runtime error:", err)
            case nil:
                fmt.Println("meiYOUCUOWU")
            default: // 非运行时错误
                fmt.Println("error:", err)
            }
        }()
        entry()
    }
    func main() {
        
        ProtectRun(func(){
            fmt.Println("没有异常出现  ")
        })
    }
    >>
    没有异常出现  
    meiYOUCUOWU
    
    • go 语言结构体

    这里简单说一下吧,因为以前看过很多里
    结构体的字段名不能重复的
    结构体定义的是一个内存结构,只有实例化才能分配内存
    1、实例化有两种形式:
    直接初始化:

    var ins T 
    ins.xx = xx  // 属性赋值 
    

    先分配内存:
    new() 或 & 即可 返回的是 结构体指针类型

    ins := &T{} // 对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
    ins := new(T)
    

    ins 现在为 *T 类型
    结构体可以不用new 来初始化,那就是普通值类型,new 之后是为引用类型了
    2、初始化结构体的成员变量
    使用“键值对”初始化结构体
    键值对就是 {key:value}, 忽略的字段自动使用 类型零 值

    (结构体成员中只能包含该结构体的指针类型,包含非指针类型会引起编译错误。)
    直接忽略键

    ins := 结构体类型名{
        字段1的值,
        字段2的值,
        …
    }
    

    这种形式需要注意,必须初始化所有值,位置也要对应好。

    • Go语言构造函数

    1、这个构造函数就是自己写的一个初始化函数罢了;

    type Cat struct {
        Color string
        Name  string
    }
    func NewCatByName(name string) *Cat {
        return &Cat{
            Name: name,
        }
    }
    func NewCatByColor(color string) *Cat {
        return &Cat{
            Color: color,
        }
    }
    

    上面定义了两个函数,对 猫对象进行初始化。
    2、 类似继承

    type Cat struct {
        Color string
        Name  string
    }
    type BlackCat struct {
        Cat  // 嵌入Cat, 类似于派生  定义 BlackCat 结构,并嵌入了 Cat 结构
    // 体,BlackCat 拥有 Cat 的所有成员,实例化后可以自由访问 Cat 的所有成员。
    }
    // “构造基类”
    func NewCat(name string) *Cat {
        return &Cat{
            Name: name,
        }
    }
    // “构造子类”
    func NewBlackCat(color string) *BlackCat {
        cat := &BlackCat{}  // 实例化 BlackCat 结构,此时 Cat 也同时被实例化
        cat.Color = color
        return cat
    }
    

    // Cat 结构体类似于面向对象中的“基类”

    • 类型内嵌和结构体内嵌

    结构体的字段有些事可以省略 字段名的,只有类型

    type innerS struct {
        in1 int
        in2 int
    }
    type outerS struct {
        b int
        c float32
        int // anonymous field
        innerS //anonymous field
    }
    func main() {
        outer := new(outerS)
        outer.b = 6
        outer.c = 7.5
        outer.int = 60  // 匿名字段的类型智能有一个哦,因为他想相当于字段名(不重复)
        outer.in1 = 5   // 可以使用 嵌套的字段(相当于继承的)
        outer.in2 = 10
        fmt.Printf("outer.b is: %d\n", outer.b)
        fmt.Printf("outer.int is: %d\n", outer.int)
        fmt.Printf("outer.in2 is: %d\n", outer.in2)  
    }
    

    在一个结构体中对于每一种数据类型只能有一个匿名字段

    • go 接口

    接口就是对外暴露的使用特性
    内部使用结构体内嵌组合对象应该具有的特性
    接口就是一组方法特征的提取,绑定各种比如结构体的对象,已实现在对象上层进行抽象的东西
    声明:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        …
    }
    

    注意 方法名和接口名首字母都大写,这个方法就可以被其他包直接调用了

    接口内的方法参数和返回值 ,可以不写变量,只写类型即可

    type writer interface{
        Write([]byte) error
    }
    
    • Go语言实现接口的条件

    一个任意类型T的方法集实现了接口A的 全部方法(>=),那么T 就实现了 A 的接口类型。
    接口的作用就是 提取,解耦,同一,规范

    // 定义一个数据写入器
    type DataWriter interface {
        WriteData(data interface{}) error
    }
    // 定义文件结构,用于实现DataWriter
    type file struct {
    }
    // 实现DataWriter接口的WriteData方法
    func (d *file) WriteData(data interface{}) error {
        // 模拟写入数据
        fmt.Println("WriteData:", data)
        return nil
    }
    func main() {
        // 实例化file
        // var f file
        // 声明一个DataWriter的接口
        var writer DataWriter
        // 将接口赋值f,也就是*file类型
        writer = new(file) // 这边要是指针类型,如果是结构体直接调用其方法,则不需要写&xx这种类型,吊用的时候会自动转化的,但如果传给接口变量,则要写&或用new返回地址
        // 使用DataWriter接口进行数据写入
        writer.WriteData("data")
        //var f file
       //  writer = f //如果是 file 类型,那么报错:file does not implement 
    //DataWriter (WriteData method has pointer receiver)
        // writer.WriteData("data")
    }
    >>
    WriteData: data
    

    要使用接口,必须实现接口内的所有方法,否则都是编译报错(实现一部分,是乱的,没法整齐划一,错误也很难检查了)

    • 类型与接口的关系

    类型可以有多个接口的,因为功能分类不一样,所以实现的接口也可以不一样的,这也体现了解耦

    • go 类型断言

    其实上面已经讲过了
    1、使用在接口值上的操作

    返回 x 的值(也就是 value)和一个布尔值(也就是 ok)
    判断失败,将会把 ok 置为 false,t 置为 T 类型的 0 值

    value, ok := x.(T)   
    

    或者只返回 转化后的值(注意错误判断)

    
        var a interface{}
        a = float32(0.1)
        v := a.(float32)
        fmt.Println(v)
    >0.1
    

    x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)

    如果 T 是具体的类型,并且 x 属于 T 类型, value 则会转化为T类型的值
    如果T 是接口类型, x 属于他, x 转化为T 的接口值
    2、通过 switch 进行判断
    pass

    • 包的使用

    1、包的一般常识
    一般小写,和目录名一样(也可以不一样)
    一个目录下属于同一个包,一个包的文件不能放在不同的文件夹下

    2、包的导入
    分全路径和相对路径的导入
    全路径:

    包名是从GOPATH/src/ 或 GOROOT/src/后开始计算的,使用/ 进行路径分隔.GOPATH/src/ 下面是自己的包,GOROOT/src/ 下面是 标准包

    import "lab/test"  // 自己的包
    import "database/sql/driver"   // 内置包
    import "database/sql"   // 内置包
    

    上面演示了导入形式
    test 包是自定义的包,其源码位于GOPATH/src/lab/test 目录下;
    driver 包的源码位于GOROOT/src/database/sql/driver 目录下;
    sql 包的源码位于GOROOT/src/database/sql 目录下。
    相对路径:
    标准包只可以却对路径导入,自己的包(gopath/src 下面的)可以使用相对路径导入(
    和 py 一样的

    两个包:
    GOPATH/src/lab/a
    GOPATH/src/lab/b
    b 中 导入a 包:

    import ../a
    或 
    import  lab/a
    

    3、包的引用格式
    四中格式
    标准:

    import "xx"
    // xx . 使用
    

    自定义别名:

    import F "fmt"
    F.  使用
    

    全导入:

    import . "fmt"
    相当于把 fmt 包直接合并到当前程序中
    直接 Println()  而不用加 fmt. 了,因为已经合并了包到当前包,同包直接用的
    

    匿名引用格式

    import _ "fmt"
    相当于只是初始化 这个包 init 函数
    

    导入包注意点:

    • 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面
    • 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
    • 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次

    4、gopath
    go 安装后,会设置一个默认地 gopath


    image.png

    在 GOPATH 指定的工作目录下,代码总是会保存在 GOPATH/src 目录下。在工程经过 go build、go install 或 go get 等指令后,会将产生的二进制可执行文件放在GOPATH/bin 目录下,生成的中间缓存文件会被保存在 GOPATH/pkg 下。 添加 git 等工具,只需要添加GOPATH/src 目录的源码即可。bin 和 pkg 目录的内容都可以由 src 目录生成。
    设置一个 gopath

    export GOPATH=xxx 
    

    执行 go install 等命令编译的时候 会自动找到当前gopath 的目录进行操作。
    包括引用的包等,也会去 gopath 里面找

    最好每个项目设置自己的gopath, 以免混乱
    5、自定义包
    自定义的包在GOPATH 的 src 目录下, 或者 src 子目录都可以的,只要编译地时候指定从 gopath/src 下的相对路径就好了
    同一包必须在同一个文件夹下,比如目录a 下有 b.go c.go d目录 那么 b, c 就是 属于同一个包。d 也许是a下面一个 单独的包.

    导入包需要注意:

    如果项目的目录不在 GOPATH 环境变量中,则需要把项目移到 GOPATH 所在的目录中,或者将项目所在的目录设置到 GOPATH 环境变量中,否则无法完成编译;
    使用 import 语句导入包时,使用的是包所属文件夹的名称;
    包中的函数名第一个字母要大写,否则无法在外部调用;
    自定义包的包名不必与其所在文件夹的名称保持一致,但为了便于维护,建议保持一致;
    调用自定义包时使用 包名 . 函数名 的方式,如上例:demo.PrintStr()

    一个目录下的同级文件归属一个包。
    包名可以与其目录不同名。
    包名为 main 的包为应用程序的入口包,编译源码没有 main 包时,将无法编译输出可执行的文件。

    6、让外部访问包的类型和值
    正常的首字母大写就ok了。
    还有一个情况,就是结构提里面的属性,首字母也要大写,否则在包外,无法调用到的(字段要包外使用,她本身大写,整个结构体名也首大写才行)

    在被导出的结构体或接口中,如果它们的字段或方法首字母是大写,外部可以访问这些字段和方法

    编译直接 写 main 包目录即可

    相关文章

      网友评论

          本文标题:go 学习笔记

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