GoLang基础(不断更新)

官网:https://golang.google.cn/

中文社区:https://studygolang.com/

中文文档:https://studygolang.com/pkgdoc

基础语法,关键字的使用就没必要赘述了,更多的是记录一些容易记混或者忘记的关键技术点!

个人学习代码记录:https://gitee.com/jiguiquan/gostudy

一、第一部分

1、init const var main 执行顺序:

这几个关键字的执行顺序,对分析Golang语言执行至关重要!有一张很经典的图:

1694065404891630.png

  • 单个包中:import —> const —> var —> init,结束初始化;

  • 多个包中:如果一个包导入了其他包,那么先完成被导入包的初始化工作

    • const:常量;

    • var:全局变量(不包含方法体中的局部变量);

    • init:初始化方法;

    • main:入口方法,一般一个程序,只有一个main方法(普通func方法,只会在被调用时触发);


二、在reflect反射场景下,我们何时应该传入值,何时应该传入指针?

反射这块内容很多,尤其在值传递和指针传递这个知识点上,与java很不一样,所以我单独提出来成一个章节!

对于网上很多帖子讲的“获取不到小写字母开头的属性或者方法”:那个算是GoLang语言的几个基础约定了,小写是private,大写是public对外暴露的,没啥好讲的!

1、什么时候需要使用Elem(),什么时候不需要使用?

我们先准备一个结构体Book,它包含3个属性,2个方法(1个指针方法,一个值方法):

type Book struct {
    Id int
    Name string
    Auth string
}

func (this Book) CallValue()  {
    fmt.Println("Book.CallValue()值调用...")
}

func (this *Book) CallPoint()  {
    fmt.Println("Book.CallValue()指针调用...")
}

我们先不使用Elem()方法,就是简单的答应看看 reflect.typeOf() 在调用指针和非指针时,到底有什么本质区别:

func main() {
    book := Book{1, "三体", "刘慈欣"}

    valueType := reflect.TypeOf(book)
    pointType := reflect.TypeOf(&book)

    fmt.Printf("一:valueType.Name = %s, valueType.Kind = %s\n", valueType.Name(), valueType.Kind())
    fmt.Printf("二:pointType.Name = %s, pointType.Kind = %s\n", pointType.Name(), pointType.Kind())
    fmt.Printf("三:pointType.Elem.Name = %s, pointType.Elem.Kind = %s\n", pointType.Elem().Name(), pointType.Elem().Kind())
}

输出结果:

一:valueType.Name = Book, valueType.Kind = struct
二:pointType.Name = , pointType.Kind = ptr
三:pointType.Elem.Name = Book, pointType.Elem.Kind = struct

我们可以得出结论:

  • 当我们在对指针参数进行反射时,直接得到的是一个ptr(pointer)类型的数据,而不是它背后真正的struct结构体!(对比一&二)

  • 我们我们想获得它背后真正的结构体,就需要使用到 Elem() 方法了,当调用了Elem()方法后,就和直接值参数一样了!(对比一&三)

既然到这里了,那我们就简单地跟踪一下源码吧:

// C:/Program Files/Go/src/reflect/type.go:897
// 显然,Elem方法会先获得真正的Kind类型,进行分支判断:如果是Ptr指针的话,那么需要获得 tt.elem ,并返回
func (t *rtype) Elem() Type {
   switch t.Kind() {
   case Array:
      tt := (*arrayType)(unsafe.Pointer(t))
      return toType(tt.elem)
   case Chan:
      tt := (*chanType)(unsafe.Pointer(t))
      return toType(tt.elem)
   case Map:
      tt := (*mapType)(unsafe.Pointer(t))
      return toType(tt.elem)
   case Ptr: 
      tt := (*ptrType)(unsafe.Pointer(t))
      return toType(tt.elem)
   case Slice:
      tt := (*sliceType)(unsafe.Pointer(t))
      return toType(tt.elem)
   }
   panic("reflect: Elem of invalid type " + t.String())
}

需要弄清楚 tt.elem 具体是啥?

// 中文翻译就是:指针指向的实际element的type
// ptrType represents a pointer type.
type ptrType struct {
   rtype
   elem *rtype // pointer element (pointed at) type // 指针指向的实际element的type
}
  • 破案了,很清晰,Elem() 方法:判断出是指针后,返回指针指向的实际element元素的type类型;

  • ValueOf(&book).Elem() 原理相同,可以自行跟踪下源码即可!

所以,咱们最终的结论是:

当反射入参为指针时,我们想获得指针背后实际element的Type或者Value时,就需要先调用Elem()方法进行一次转化!

先这么简单理解吧!

2、指针类型获得属性数量及属性列表:

还是使用上面的Book结构体,先来个错误的写法,获取属性数量:

func main() {
    book := Book{1, "三体", "刘慈欣"}

    valueType := reflect.TypeOf(book)
    pointType := reflect.TypeOf(&book)

    valueNumField := valueType.NumField()
    pointNumField := pointType.NumField()

    fmt.Printf("valueType.NumField = %d", valueNumField)
    fmt.Printf("pointType.NumField = %d", pointNumField)
}

此方法会直接报错: panic: reflect: NumField of non-struct type *main.Book 

显然,non-struct type 代表:指针不是一个结构体type,所以不允许调用NumField!

简单修改下,使用Elem就没有问题了,就不贴代码了,和后面的案例一起体现:

func main() {
    book := Book{1, "三体", "刘慈欣"}

    pointType := reflect.TypeOf(&book)
    pointValue := reflect.ValueOf(&book)

    numField1 := pointType.Elem().NumField()
    numField2 := pointValue.Elem().NumField()

    fmt.Printf("valueType.NumField = %d\n", numField1)
    fmt.Printf("pointValue.NumField = %d\n", numField2)

    for i := 0; i < numField1; i++ {
        field := pointType.Elem().Field(i)
        value := pointValue.Elem().Field(i).Interface()
        fmt.Printf("属性:%s: %v = %v\n", field.Name, field.Type, value)
    }
}

执行结果如下:

valueType.NumField = 3
pointValue.NumField = 3
属性:Id: int = 1
属性:Name: string = 三体
属性:Auth: string = 刘慈欣

由此,我们可以得出结论:

  • 在想获得属性个数时,我们既可以使用 TypeOf() 也可以使用 ValueOf();

  • 当想获得结构体属性的列表时,我们需要用到 TypeOf() 的返回结果,但是如果想获得具体的值,就需要用到 ValueOf() 的返回值!

其实,反射获取属性列表还是比较容易处理的,一般不会出现什么错误,但是当获取方法列表的时候,可能就会遇到错误了!

3、对比反射调用对象方法时,值入参和指针入参的区别:

还是使用上面的Book结构体,先看一个简单的案例:

func main() {
    book := Book{1, "三体", "刘慈欣"}

    valueType := reflect.TypeOf(book)
    pointType := reflect.TypeOf(&book)

    numMethod1 := valueType.NumMethod()
    numMethod2 := pointType.NumMethod()

    fmt.Printf("valueType.NumField = %d\n", numMethod1)
    fmt.Printf("pointType.NumField = %d\n", numMethod2)

    for i := 0; i < numMethod1; i++ {
        method := valueType.Method(i)
        fmt.Printf("valueType方法:%s:%s\n", method.Name, method.Type)
    }

    for i := 0; i < numMethod2; i++ {
        method := pointType.Method(i)
        fmt.Printf("pointType方法:%s:%s\n", method.Name, method.Type)
    }
}

执行的结果如下:

valueType.NumField = 1
pointType.NumField = 2
valueType方法:CallValue:func(main.Book)
pointType方法:CallPoint:func(*main.Book)
pointType方法:CallValue:func(*main.Book)

显然:

  • 通过实际值传参,通过反射时,我们是没有办法获取到指针类型的方法的;

  • 而通过指针传参,通过反射时,我们就可以获取到所有的方法;

所以,如果我们想通过获取全部的方法时,就一定要使用指针传参!

那如果我们想调用Book中的方法呢?

func main() {
    book := Book{1, "三体", "刘慈欣"}

    pointType := reflect.TypeOf(&book)
    pointValue := reflect.ValueOf(&book)

    numMethod := pointType.NumMethod()

    for i := 0; i < numMethod; i++ {
        method := pointValue.Method(i)
        // 因为我们的方法没有入参,所以我就定义了一个空的params,如果有入参,就要通过make开辟空间,放入参数了
        var params []reflect.Value
        method.Call(params)
    }
}

执行结果如下:

Book.CallValue()指针调用...
Book.CallValue()值调用...

调用没有问题!

正常工作中肯定不可能是像上面那样遍历调用所有的方法,而是通过 .MethodByName("") 得到具体的 method 后,传入具体的参数列表,进行调用!


三、为什么要用Go Modules取代GOPATH 工作模式

1、何为GOPATH工作模式?

首先我们可以使用 go env查看或设置当前的环境变量:

# 查看环境变量
C:\Users\jiguiquan> go env
....
set GOPATH=D:\GOPATH
set GOPROXY=https://goproxy.cn
set GOROOT=C:\Program Files\Go
...

## 设置环境变量
C:\Users\jiguiquan> go env -w GOPROXY=https://goproxy.cn

可以看到我们设置的GOPATH路径!进入到这个目录下,我们可以看到3个经典子目录:

go
├── bin    # 存储所编译生成的二进制文件(执行go build后生成的二进制文件)
├── pkg    # 存储预编译的目标文件,以加快程序的后续编译速度
└── src    # 存储所有.go文件或源代码

在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src目录下,并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。

2、GOPATH模式的弊端:

在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有以下弊端:

  • 无版本控制概念:在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本;

  • 无法同步一致第三方版本号:在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致;

  • 无法指定当前项目引用的第三方版本号:你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar,本地安装的是什么版本,项目引用的就是什么版本,完全不由项目自己决定;

3、Go Modules 模式的引入——常用的命令:

命令 作用
go mod init 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块

4、Go Modules 模式重要的环境变量:

以下环境变量对Go Modules模式很重要:

GO111MODULE="auto"
GOPROXY="https://goproxy.cn"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
  • GO111MODULE

    Go语言提供了 GO111MODULE 这个环境变量来作为 Go modules 的开关,其允许设置以下参数:

    • auto:只要项目包含了 go.mod 文件的话启用 Go modules(默认值);

    • on:启用 Go modules,推荐设置,将会是未来版本中的默认值;

    • off:禁用 Go modules,不推荐设置。

    # 我们建议手动将该值设为on
    go env -w GO111MODULE=on
  • GOPROXY

    这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取,一般都会将其设置为国内的代理地址:

    • 阿里云:https://mirrors.aliyun.com/goproxy/ 

    • 七牛云:https://goproxy.cn,direct

    # 最后的direct为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等);
    ## 当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个;
    ### 遇见 “direct” 时回源,遇见 EOF 时终止并抛出类似“invalid version: unknown revision…”的错误;
    go env -w GOPROXY=https://goproxy.cn,direct
  • GOSUMDB

    它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止;

    GOSUMDB 的默认值为:sum.golang.org,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database);

    因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn 就能支持代理 sum.golang.org,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心;

  • GONOPROXY/GONOSUMDB/GOPRIVATE

    这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败;

    更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。

而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议的最佳姿势是直接使用 GOPRIVATE,如:

$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
# 设置后,前缀为 git.example.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块

如果不想每次都重新设置,我们也可以利用通配符,例如:

$ go env -w GOPRIVATE="*.example.com"
# 设置后,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database;
## 需要注意的是不包括 example.com 本身。

5、使用Go Modules模式初始化我们的项目:

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐