go 学习笔记

这篇文章就总结一下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接口类型的使用
go 中文网 接口的作用
github go 做web

这个就比较全, awesome go 文档

Go 语言问题集(Go Questions)

  • 数组声明
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]
数组直接切片就是返回的就是切片类型, 他是原来数组的一个引
用,如果原来数组改变,切片的值也是跟着变的

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

var a = [5] int {0,1,2,3,4}
b := a[2:3]
b = append(b, 33)
fmt.Println(cap(b), a)
cap(b) 结果是 3  a 的结果是  [0 1 2 33 4]

为啥上面的 cap(b) 而不是 1, 因为如果是用数组进行切片的,cap 默认是当前切片索引的第一个数字到数组结束的位置,所以进行 append 之后, a 的第四数字 3 被 33覆盖,如果,append(...) 导致 b cap 超出,他会切换地址,就不会影响 a 的值了,有意思。

一般 [ 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 >函数结束

命名返回值变量, defer 会修改他的值
匿名返回值变量, defer 不会修改他的值

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")
    }
}

有时候程序需要打印出异常堆栈
https://github.com/pkg/errors
https://www.jianshu.com/p/1fd51b1706e2
使用 github.com/pkg/errors 给 error 变量添加堆栈,然后打印,如下:

if err := recover(); err != nil{
            fmt.Printf("%+v", errors.WithStack(err.(error)))   // %+v 为输出堆栈格式, 这里 err  为 接口类型,所以转为 WithStack 需要的 error 类型,
// 如果主动 抛出panic 错误,那么请 这样 panic(errors.New("some desc"))  而不是 panic("some desc")   (ps: 这种panic  recover 捕获 是字符串 接口类型了,err.(error) 会报错)
}
>> 
runtime error: invalid memory address or nil pointer dereference
check_server/user.Register.func1
    D:/bbk/无人机项目/user/register.go:146
...
    D:/bbk/无人机项目/user/register.go:161
github.com/gin-gonic/gin.(*Context).Next

这个暂时不太懂啊
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 已经有了 地址,只不过他里面的 属性都是零值, 数值类型零值一般不是 nil ) 
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
}

( 有个地方需要注意 type xxx string, interface{}(xxx).(type) 值是xxx 了 而不是 string)

  • 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、包的导入 (现在我使用go module,所以这块不需要看了)
分全路径和相对路径的导入
全路径:

包名是从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/bin 目录下,生成的中间缓存文件会被保存在 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 学习笔记)