这篇文章就总结一下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
- 数组声明
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
//www.greatytc.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
在 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 包目录即可