本篇文章站在一个Java程序员的角度来认识Go语言。在介绍标题所提到的三部分内容之前首先来看一下语言的整体结构,以经典的“Hello,World!”为例:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, World!")
}
引入报名是使用的关键字package
,每一个.go
文件都必须在首行指出属于哪个包,并且每一个Go程序都要包含一个main
包。
引入包使用的是关键字import
,后面跟小括号()
,在里面使用双引号""
包裹的是要引入的包。如果只引入了一个包可以不加小括号。需要注意的是,Go语言会强制规范代码格式,在一些情况下保存文件会自动格式化,在另一些情况下比如声明了变量但没有使用是会报错的。
func
关键字是声明函数的关键字,在Go程序中main
函数作为程序的入口同样是不能缺少的。例子中的main
函数是一个不接受参数不返回参数的函数。在main
方法中调用了fmt
库中的Println
函数来输出文本。在Go中没有标识作用范围的关键字(如Java中的public
,private
),当一个标识符(如变量、常量、函数名,结构体等等)以首字母大写开头时代表它能被其他包所访问。例子中的Println
函数就是一个大写开头的函数名。
函数的入参和返回值的书写也与Java略有不同:
func myFunction(name string) string {
var myName string
myName = name
return myNamee
}
上面一段代码表明它是一个包内的私有函数(即便被其他包引用也不能访问该函数),接收一个string
类型的参数,形参名为name
,并返回一个string类型的返回值。由于Go中并没有类这个概念,所以在调用函数时直接调用即可myFunction("ah")
。像Java中类的功能方式是由结构体和方法完成的,这一点以后再说。
介绍完基本的结构下面来介绍一下go的目录结构和包管理。文内不打算专门介绍变量和控制语句等,这些内容有过其他语言基础的同学在示例代码中看一遍就明白了,不过在这里提一句,go语言中没有while
循环语句。
在高版本(1.14及以后)的go中可以使用Go Modules来对项目进行包管理。在当前目录下执行命令
go mod init nameYouLike
执行完后会在当前目录下生成一个go.mod
文件。这个文件可以看作Java中使用Maven进行包管理时的pom.xml
文件。
使用go mod的方式很简单,比如现在要引入一个第三方的包gin,那么只需要在文件中的import
内拼出正确的名字后,执行命令go mod tidy
(1.16以后需要手动执行,在此前输入完保存后会自动下载。如果你执行失败可能时与github的连接状况不好)后会发现多出了一个go.sum
文件,同时go.mod
文件的内容也发生了变化:
module goBase
go 1.17
require github.com/gin-gonic/gin v1.7.7
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
其中go.sum
文件包含特定模块版本内容的预期加密哈希,是系统自动维护的所以不需要关注。现在就能正常的使用刚才引入的包gin
了:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("Hello, World!")
r := gin.Default()
r.Run()
}
这里说明一下,声明并初始化变量可以使用:=
。
s := "abc"
//两种方式等价
var s string
s = "abc"
上面是引用第三方包的使用方式,在引用项目中自定义的包时还有一些注意事项。在项目中新建文件夹demo
,在文件夹下新建文件demo.go
,包名设置为demo
。demo.go
文件内容如下:
package demo
import "fmt"
func Demo() {
fmt.Println("demo")
}
此时的项目目录结构如下:
--demo
--demo.go
go.mod
go.sum
main.go
在main
函数中引用时需要注意包的前缀名必须是在go.mod
中声明的名称:
package main
import (
"fmt"
"goBase/demo"
)
func main() {
demo.Demo()
}
关于一个项目的目录结构,我想并没有一个通解,而是根据不同的项目选择合适的结构。下面是golang-standards给出的一个模板:
├── api
├── assets
├── build
│ ├── ci
│ └── package
├── cmd
│ └── _your_app_
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── init
├── internal
│ ├── app
│ │ └── _your_app_
│ └── pkg
│ └── _your_private_lib_
├── pkg
│ └── _your_public_lib_
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
│ ├── app
│ ├── static
│ └── template
├── website
├── .gitignore
├── LICENSE.md
├── Makefile
├── README.md
└── go.mod
当然了,你还可以在github中找一些go项目来看一看其他人是如何管理目录结构的。
虽然同样有垃圾回收机制,但是不同于Java语言,Go语言中是存在指针的。如果你有过C\C++的基础,那么相信对指针这个概念并不陌生。C中的指针和Go中的指针是说的同一个东西:一个指向变量地址的变量(不同的地方在于Go中的指针不能进行便偏移和运算,是安全指针)。下面我假设你是一个没有使用过指针的程序员来对指针进行介绍。
正如上面说到的一样,指针就是指向一个变量的内存地址的变量。指针是一个变量。 它的值(存放的内容)是另外一个变量的内存地址。
以上图为例(假设内存地址按1递增),a、b、c三个为变量名,后面是它的值,再后面是它实际的内存地址。可以看到变量c的值实际上是变量b的内存地址,那么就可以说c是指向b的指针。
在Java中没有指针,但是并不影响使用这个语言来完成一些很复杂的工作。那么指针是不是可以说可有可无呢?实际上Java中并不是没有指针,只不过是没有开放出来供程序员使用,你在开发Java程序的过程中可能经常会遇到这个报错信息:NullPointerException
空指针异常。
为什么要用指针?因为在Go中函数传参都是值拷贝,即函数并不能改变传入的参数本身。但是有些时候我们确实需要对参数本身进行修改,那么这个时候就可以通过使用一个指向这个参数的指针来对它进行修改。如果你有了这个参数的内存地址,那么当然可以对它进行任意操作了(类似于你有了数组中一个数组变量的下标)。
使用指针只需要记住两个符号:*
(根据地址取值)和&
(取地址)。Go中将&
放在一个变量前面就是对这个变量进行取地址操作,就是说拿到这个变量得到内存地址(还是以数组为例,这个操作类似于取数组的下标)。语法如下:
ptr := &v
其中v
是一个变量,它可以是任意类型,int、string或者是自定义的Student类型等等,假设它是T
类型。ptr
是用于接收v
地址的变量,ptr
的类型就是*T
。取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。下面来看一段代码说明如何使用:
func main() {
a := 10 //初始化变量a
b := &a // 取变量a的地址,将地址保存到b中。此时可以称b是a的指针。
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值,根据指针b去内存取值。b是一个指针,所以实际上取的是b所指向的变量也就是a
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
如果你对注释的内容还不太明白,那么再以数组进行类比:a是一个数组变量,b是a的下标,c就是一个新的变量来根据数据下标取值,取到的是数组变量a的值。
Go中的new
和Java中的new
不太一样,它是作为一个内置函数而不是一个关键字。new
后面接收一个类型进行初始化,返回一个地址。实际上,new
常用于初始化结构体。
make
也是一个内置函数,对参数进行初始化并返回一个类型本身。make
只用于初始化slice、map以及chan,这三个是Go内置的基本数据结构。
func main() {
fmt.Println("Hello, World!")
a := new(map[string]int)
fmt.Println(a)
fmt.Printf("type of a:%T", a)
b := make(map[string]int)
fmt.Println(b)
fmt.Printf("type of b:%T", b)
}
Go内置的基本数据结构并不多,下面来介绍三种:数组、切片和Map,前者是值类型,后两者是引用类型,对这两种数据类型进行的操作会影响其本身:
func main() {
ma := make(map[string]int)
ma["age"] = 1
fmt.Println(ma) //初始值为1
changeAge(ma)
fmt.Println(ma) //使用changeAge函数后变为2
}
func changeAge(ma map[string]int) {
ma["age"] = 2
}
数组类型是值类型,所以赋值和传参时不会改变其本身的值。
初始化一维数组的很简单:
package main
import (
"fmt"
)
var arr0 [5]int = [5]int{1, 2, 3} //[size]中括号中的值确定数组的大小
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [5]int{} //初始化但不赋值
var arr3 = [...]int{1, 2, 3, 4, 5, 6} //[...]代表不定长度的数组
var str = [5]string{3: "hello world", 4: "tom"}
func main() {
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
fmt.Println(arr0, arr1, arr3, str)
fmt.Println(a, b, c)
arr2[0] = 1
arr2[1] = 2
arr2[2] = 3
fmt.Println(arr2)
}
初始化多维数组:
package main
import (
"fmt"
)
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
func main() {
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
fmt.Println(arr0, arr1)
fmt.Println(a, b)
}
如果你还记得之前说的指针操作的话,需要的时候可以传递数组指针来直接操作数组本身,另外传递数组指针还可以避免数组过大时值拷贝造成的性能影响。
可以使用内置函数len
来求数组长度:len(arr)
。
遍历数组多使用range
,当然也可以使用数组下标的方式来遍历:
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
//使用range遍历二维数组
var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
for k1, v1 := range f {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
}
fmt.Println()
}
}
切片不是数组,它类似于Java中的ArryList
。切片的长度是可变的,同样可以使用len
来求切片长度,创建切片时不需要指定长度。使用append
来向切片尾增加元素。
package main
import "fmt"
func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// 4.初始化赋值。第三个参数“容量”是可选参数,表示切片最大有多大,超过此值会自动扩容
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
s5 := []int{1, 2, 3}
fmt.Println(s5)
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
// 前包后不包
s6 = arr[1:4]
fmt.Println(s6)
//去掉切片最后一个元素
var s7 = arr[:len(arr)-1]
fmt.Println(s7)
}
切片的遍历方式和数组类似,可以使用循环遍历或者range
遍历,不再赘述。
Go中的map
同样是一个键值对映射数据结构,下面来看一下如何初始化和一些基本操作。
初始化map
的方式比较简单:
func main() {
nameToAge := make(map[string]int)
nameToAge ["张三"] = 18
nameToAge ["李四"] = 25
nameToAge ["王五"] = 30
fmt.Println(nameToAge["张三"])
//声明时填充元素
userInfo := map[string]string {
"username": "three",
"password": "123456",
}
fmt.Println(userInfo)
}
接下来看一下如何使用map
。
func main() {
nameToAge := make(map[string]int)
nameToAge ["张三"] = 18
nameToAge ["李四"] = 25
nameToAge ["王五"] = 30
// 如果key存在ok为true,value为对应的值;不存在ok为false,value为值类型的零值
value, ok := nameToAge["张三"]
if ok {
fmt.Println(value)
} else {
fmt.Println("查无此人")
}
}
func main() {
nameToAge := make(map[string]int)
nameToAge ["张三"] = 18
nameToAge ["李四"] = 25
nameToAge ["王五"] = 30
//按key遍历map,打印出key和其对应的value
for k, v := range nameToAge {
fmt.Println(k, v)
}
//只遍历key
for k := range nameToAge {
fmt.Println(k)
}
}
func main() {
nameToAge := make(map[string]int)
nameToAge ["张三"] = 18
nameToAge ["李四"] = 25
nameToAge ["王五"] = 30
delete(nameToAge, "张三")
for k, v := range nameToAge {
fmt.Println(k, v)
}
}
当然还可以在切片中嵌套map
,或者在map
中嵌套切片,具体的使用方法可以自己探索一下。