鉴于上篇文章我们已经讲过Go语言环境的安装,现在我们已经有了一个可以运行Go程序的环境,而且,我们还运行了'Hello World'跑出了我们的第一个Go程序。
这节我们就以'Hello World为例,讲解Go的基础结构,详细的解释一下Hello World中的每一行都代表了什么。
Go语言是一门静态语言,在编译前都会对代码进行严格的语法校验,如果语法错误是编译不过去的,所以基础结构是非常重要的一个环节。
<!--more-->
类似Java中的package、class、interface、函数、常量和变量等,Go也有package、struct、interface、func、常量、变量等内容。
struct类似Java中的class,是Go中的基本结构题。
interface也类似Java中的接口,可定义函数以供其他struct或func实现(可能不太好理解,后面会讲)。
这里我们按照由外而内,从上至下的的顺序进行讲解。
GOPATH
上节说到GOPATH指定了存放项目相关的文件路径,下面包含'bin'、'pkg'、'src'三个目录。
1.src 存放项目源代码
2.pkg 存放编译后生成的文件
3.bin 存放编译后生成的可执行文件
目录结构如下
GOPATH
\_ src
\_ projectA
\_ projectB
\_ pkg
\_ projectA
\_ projectB
\_ bin
\_ commandA
\_ commandB
src目录是我们用的最多的,因为我们所有的项目都会放到这个目录中。后续项目的引用也是相对于该目录。
文件名
一个Go文件,我们对其的第一个认识,就是其的后缀,Go文件以'.go'结尾(Windows用户一定要文件后缀的问题),这个很容易理解。
不能以数字开头、不能包含运算符、不能使用Go的关键字等对比其他语言也非常容易理解。
有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _)开头加上任意个数字或字符,如:'hello_world.go'、'router.go'、'base58.go'等。
Go天然支持UTF8
不过在这里有一些细节需要注意,就是Go文件在命名的时候,跟其他语言不太一样。
Go文件都是小写字母命名(虽然大写也支持,但是大写文件名并不规范),如果需要多个单词进行拼接,那单词之间以_
下划线进行连接。
特别是编写测试用例和多平台支持时对Go文件的命名。
如:'math_test.go'、'cpu_arm.go'、'cpu_x86.go'等。
命名规范
Go可以说是非常干净整洁的代码,所以Go的命名非常简洁并有意义。虽然支持大小写混写和带下划线的命名方式,但是这样真的是非常的不规范,Go也不推荐这样做。Go更喜欢使用驼峰式的命名方式。如'BlockHeight'、'txHash'这样的定义。另外Go的定义可以直接通过'package.Xxx'这样的方式进行使用,所以也不推荐使用GetXXX这样的定义。
package
package的存在,是为了解决文件过多而造成的絮乱等问题,太多的文件都放在项目根目录下看着总是让人觉得不舒服,对开发、后期维护等都是问题。
作为代码结构化方式之一, 每个Go程序都有package的概念。
Go语法规定
- 每个Go文件的第一行(不包含注释)都必须是package的定义
- 可执行的程序package定义必须是main
- package默认采用当前文件夹的名字,不采用类似Java中package的级联定义
如下面的代码指定该文件属于learning_go这个package。
package learning_go
...
Go对package的定义并不严格,在文件夹下的Go文件可以不使用文件夹名称作为package的定义(默认使用),但是同一个文件夹下的所有Go文件必须使用同一个package定义,这个是严格要求的。
tips: package的定义均应该采用小写字母,所以文件夹的定义也应该都是小写字母。
为了更好的区分不同的项目,Go定义的项目结构中都包含开源站点的信息,如github、gitee等开源站点中的所有开源Go项目都可以直接拿来使用。
Go的背后是所有开源世界
拿在GitHub下面创建的Go项目gosample项目为例。其项目路径为:"github.com/souyunkutech/gosample"。"github.com"表示其开源站点信息。"souyunkutech/gosample"就是项目的名称。
文件的路径就对应为"$GOPATH/src/github.com/souyunkutech/gosample"。
"souyunkutech"表示了gosample这个项目属于"souyunkutech"这个用户所有。
package的获取
在引用某个项目之前,需要先获取其源码,将其放置到$GOPATH/src
下,这样我们才能对其进行引用从而正确的进行编译。
Go获取源码可以手动将源码根据其相对路径放到正确的路径下,也可以通过go get path
的方式自动获取。
如获取'gosample'项目,可以通过git clone
的方式下载到$GOPATH/src/github.com/souyunkutech/
也可以通过go get github.com/souyunkutech/gosamp
的方式进行获取,go会自己把项目方到$GOPATH/src/github.com/sirupsen/
目录下。
如获取Go语言中一个非常知名的第三方日志组件logrus,可以将其通过git clone
的方式下载到$GOPATH/src/github.com/sirupsen/
也可以通过go get github.com/sirupsen/logrus
的方式进行获取。
package的引用
import关键字的作用就是package的引用。作为外部引用的最小单位,Go以package为基础,不像Java那样,以对象为基础,供其他程序进行引用,import引用了某个package那就是引用了这个package的所有(可见的)内容。
语法上需要注意的就是在引用时需要双引号包裹。没有使用过的package引用要不删除,要不定义为隐式引用。否则的话,在运行或者编译程序的时候就会报错:imported and not used:...
如HelloWorld代码中引用的'fmt'就是Go语言内建的程序package。fmt这个package下包含'doc.go'、'format.go'等(这个可以通过IDE的方式进行查看)这些Go文件中所有的(可见的)内容都可以使用。
前面说到import的引用默认都是相对于$GOPATH/src
目录的。所以我们要想使用某个开源项目,就需要使用其相对于GOPATH/src
的相对路径进行引用。(Go系统内建的项目除外)
import引用的语法有两种方式
方式1,默认的引用方式,每个引用单独占一行, 如:
import "fmt"
方式2,通过括号将所有的引用写在一个import中,每个引用单独占一行。通过这种方式,Go在格式化的时候也会将所有的引用按照字母的顺序进行排序,所以看起来特别的清晰。如:
import (
"fmt"
"math"
)
比如,logrus项目结构是'github.com/sirupsen/logrus',所以在引用这个日志组件时,就要写成下面这样
import "github.com/sirupsen/logrus"
如果只想使用logrus项目中的某一个package的话,可以单引用该package,而不用引用logrus全项目。这也是非常的方便。
比如要使用logrus项目中'hook/syslog/syslog.go',就可以像下面这样写import
import "github.com/sirupsen/logrus/hooks/syslog"
Go的引用还支持以文件路径的方式。如'./utils'引用当前目录下的util这个package时,就可以写成下面这样,当然按照项目路径的方式写才是最合适最规范的,并不推荐使用文件路径进行引用。
import "./utils"
隐式引用
Go的引用有一个特别另类的支持,那就是隐式引用,需要在引用前面加上_
下划线标识。这种类型的引用一般会发生在加载数据库驱动的时候。如加载MySQL数据库驱动时。因为这些驱动在项目中并不会直接拿来使用,但不引用又不行。所以被称为隐式引用。
import _ "github.com/go-sql-driver/mysql"
package在引用的过程需要注意不能同时引用两个相同的项目,即不管项目的站点和项目所属,只要引用的项目package名称相同,都是不被允许的,在编译时会提示'XXX redeclared as imported package name'错误。但隐式引用除外。
import "encoding/json"
import "github.com/gin-gonic/gin/json"//!!! 不允许 !!!
但是能不能使用还要看这个package,就是这个package的可见性。
可见性
可见行可以理解为Java 中的私有和公有的意思。以首字母大写的结构体、结构字段、常量、变量、函数都是外部可见的,可以被外部包进行引用。如"fmt"中的"Println"函数,其首字母大写,就是可以被其他package引用的,"fmt"中还有"free"函数,其首字母小写,就是不能被其他package引用。
但是不管对外部package是否可见,同一个package下的所有定义,都是可见并可以引用、使用的。
函数
在Go语言中,函数的使用是最频繁的,毕竟要将代码写的清晰易懂嘛,而且好像所有的编程语言都有函数的概念(目前没听说过没有函数概念的语言)
在Go中,函数的定义支持多个参数,同样也支持多个返回值(比Java要厉害哦),多个参数和多个返回值之间使用英文逗号进行分隔。
同样与Java类似,Go的函数体使用'{}'大括号进行包裹,但是,Go的定义更为严格,Go要求左大括号'{'必须与函数定义同行,否则就会报错:'build-error: syntax error: unexpected semicolon or newline before {'。
- 多个参数的函数定义
func methodName(param1 type, param type2, param type3){
...
}
//简写方式,可以将相同类型的参数并列定义
func methodName(param1, param2 type, param3 type2) {
...
}
- 有返回值的函数定义
函数返回值的定义可以只定义返回类型,也可以直接定义返回的对象。
定义了返回的对象后,就可以在函数内部直接使用该定义,避免了在函数中多次定义变量的问题。同时,在返回的时候也可以单单使用一个'return'关键字来代替 'return flag'这样的返回语句。
需要注意返回值要不都定义返回对象,要不都不定义,Go不允许只定义部分函数返回对象。
//最简单的函数定义
func methodName(){
...
}
//仅定义返回的类型
func methodName() bool{
...
return false
}
// 定义返回的对象+类型
func methodName() (flag bool){
...
return
}
//定义多个返回类型
func methodName()(bool, int) {
...
return false, 0
}
//定义多个返回对象+类型
func methodName()(flag bool, index int) {
...
return
}
// !!! 不允许的定义 !!!
func methodName()(bool, index int){
...
return
}
// !!! 不允许的定义 !!!
func methodName()
{
...
return
}
在Go中有两个特别的函数,'main'函数和'init'函数。
'main'函数是程序的主入口,package main必须包含该函数,否则的话是无法运行的。在其他的package中,该函数之能是一个普通的函数。这点要特别注意。
它的定义如下,不带也不能带任何的参数和返回值
func main(){
...
}
'init'函数是package的初始化函数,会在执行'main'函数之前执行。
同一个package中可以包含多个init函数,但是多个init函数的执行顺序Go并没有给出明确的定义,而且对于后期的维护也不方便,所以一个package中尽可能的只定义一个init函数比较合适。
多个package中的init函数,按照被import的导入顺序进行执行。先导入的package,其init函数先执行。如果多个package同时引用一个package,那其也只会被导入一次,其init函数也只会执行一次。
它的定义和main函数相同,也是不带也不能带任何的参数和返回值
func init(){
...
}
数据类型
在Go中,有
- 基本类型:int(整型)、float(浮点型)、 bool(布尔型)、 string(字符串类型)
- 集合类型:array(数组)、 slice(切片)、 map(map)、 channel(通道)
- 自定义类型: struct(结构体)、func(函数)等
- 指针类型
Go的集合类型并没有Java那么复杂,什么线程安全的集合、有序的集合都没有(全都需要自己实现^_^)!
array和slice这两种集合都类似Java中的数组,他们无论在使用上都是一样的。以至于会经常忘记他们两个到底哪里不一样。其实真的是非常的细节,array是有长度的,slice是没有长度的。他们的区别就是这么小。
channel是Go中特有的一种集合,是Go实现并发的最核心的内容。与Unix中的管道也是非常的类似。
struct结构体简单理解就是对象了,可以自己定义自己需要的对象进行结构化的使用。和Java中的class不同,Java中函数是写在class中的,在Go中,struct的函数是要写在struct外的。
结构体定义需要使用type关键字结合struct来定义。struct前的字段为新的结构体的名称。内部字段可以使用大写字母开头设置成对外部可见的,也可以使用小写字母开头设置成对外部不可见。格式如下:
type Student struct {
Name string
age int
classId int
}
func main(){
var student Student = Student{Name:"小明",age: 18, classId: 1}
fmt.Printf("学生信息: 学生姓名: %s, 学生年龄: %d, 学生班级号: %d ", student.Name, student.age, student.classId)
}
针对结构体,Go还支持如下的定义
type MyInt int
这是自定义的int类型,这样做的目的是,MyInt既包含了现有int的特性,还可以在其基础上添加自己所需要的函数。这就涉及到结构体的高级语法了,后续我们会详细的介绍。
Go的作者之前设计过C语言,或许这是Go同样有指针类型的原因吧,不过讲真的,Go中的指针比C中的指针要好理解的多。在定义时简单在类型前面加上*
星号就行,使用时正常使用,不需要加星号。对其的赋值需要使用&
将其地址复制给该指针字段。
var log *logrus.Logger
func init(){
log = logrus.New()
}
func main(){
log.Println("hello world")
}
类型的内容还是很多的,不同的类型无论是在定义上还是使用上等都有不同的语境,后续会专门对其进行介绍。今天先介绍一下类型的定义。
Go中,无论是常量还是变量等的定义语法都是一样的。
常量的定义使用 const 关键字,支持隐式的定义,也可以进行多个常量的同时定义。
const PI float = 3.1415926 //显式定义
const SIZE = 10 //隐式定义
//多个常量同时定义
const (
LENGTH = 20
WIDTH = 15
HEIGHT = 20
)
//另一种写法的常量同时定义
const ADDRESS, STREET = "北京市朝阳区望京SOHO", "望京街1号"
变量的定义使用 var 关键字,同样支持隐式的定义和多个变量的同时定义
var word = "hello"
var size int = 10
var (
length = 20
width = 15
height = 20
)
var address, street = "北京市朝阳区望京SOHO", "望京街1号"
Go还支持在定义变量时把声明和赋值两步操作结合成一步来做。如下:
size := length * width * height
这样省了定义变量这一步,代码更简洁,看着也更清晰,是比较推荐的方法。(常量不能这样用哦)
关键字及保留字
为了保证Go语言的简洁,关键字在设计时也是非常的少,只有25个。
break | case | chan | const | continue |
---|---|---|---|---|
default | defer | else | fallthrough | for |
func | go | goto | if | import |
interface | map | package | range | return |
select | struct | switch | type | var |
当然,除了关键字,Go还保留了一部分基本类型的名称、内置函数的名称作为标识符,共计36个。
append | bool | byte | cap | close | complex |
---|---|---|---|---|---|
complex64 | complex128 | copy | false | float32 | float64 |
imag | int | int8 | int16 | int32 | int64 |
iota | len | make | new | nil | panic |
println | real | recover | string | true | |
uint16 | uint32 | uint64 | uint | uint8 | uintptr |
另外,_
下划线也是一个特殊的标识符,被称为空白标识符。所以,他可以像其他标识符那样接收变量的声明和赋值。但他的作用比较特殊,用来丢弃那些不想要的赋值,所以,使用_
下划线来声明的值,在后续的代码中是无法使用的,当然也不能再付给其他值,也不能进行计算。这些变量也统一被称为匿名变量。
总结
到这里,本篇内容讲解了Go中的package、func以及类型三部分的内容。也就是这三部分内容,构成了Go语言的基础结构。到这,咱们也能对 Hello World的代码有了一个清晰的认识。也可以尝试着动手写一写简单的例子来加深印象。下面是使用变量、常量、以及函数等基础结构来实现的程序,可以参考来理解。源码可以通过'github.com/souyunkutech/gosample'获取。
//源码路径:github.com/souyunkutech/gosample/chapter3/main.go
package main //定义package为main才能执行下面的main函数,因为main函数只能在package main 中执行
//简写版的import导入依赖的项目
import (
"fmt" //使用其下的Println函数
"os" //使用其下的Stdout常量定义
"time" // 使用time包下的时间格式常量定义RFC3339
"github.com/sirupsen/logrus" //日志组件
"github.com/souyunkutech/gosample/util" //自己写的工具包,下面有自定义的函数统一使用
)
//声明log变量是logrus.Logger的指针类型,使用时不需要带指针
var log *logrus.Logger
// 初始化函数,先于main函数执行
func init() {
log = logrus.New() //使用logrus包下的New()函数进行logrus组件的初始化
log.Level = logrus.DebugLevel //将log变量中的Level字段设置为logrus下的DebugLevel
log.Out = os.Stdout
log.Formatter = &logrus.TextFormatter{ //因为log.Formatter被声明为指针类型,所以对其赋值也是需要使用‘&’关键字将其地址赋值给该字段
TimestampFormat: time.RFC3339, //使用time包下的RFC3339常量,赋值时如果字段与大括号不在一行需要在赋值后面添加逗号,包括最后一个字段的赋值!!!
}
}
//定义常量PI
const PI = 3.1415926
//定义Student结构体,可以统一使用该结构来生命学生信息
type Student struct {
Name string //姓名对外可见(首字母大写)
age int //年龄不能随便让人知道,所以对外不可见
classId int //班级也是
}
//main函数,程序执行的入口
func main() {
var hello = "hello world" //定义hello变量,省略了其类型string的声明
fmt.Println(hello) //使用fmt包下的Println函数打印hello变量
//多个变量的定义和赋值,使用外部函数生成
length, width, height := util.RandomShape() //使用其他package的函数
//多个变量作为外部函数的参数
size := util.CalSize(length, width, height)
log.Infof("length=%d, width=%d, height=%d, size=%d", length, width, height, size) //使用日志组件logrus的函数进行打印长宽高和size
var student = Student{Name: "小明", age: 18, classId: 1} //声明学生信息,最后一个字段的赋值不需要添加逗号
log.Debugf("学生信息: 学生姓名: %s, 学生年龄: %d, 学生班级号: %d ", student.Name, student.age, student.classId) //使用日志组件logrus的函数进行打印学生信息
}
运行结果如下:
hello world
INFO[0000] length=10, width=15, height=20, size=3000
DEBU[0000] 学生信息: 学生姓名: 小明, 学生年龄: 18, 学生班级号: 1
如果还有不理解的内容可以通过搜云库技术群进行讨论或者留言,我们都会进行解答。
源码可以通过'github.com/souyunkutech/gosample'获取。
微信公众号
首发微信公众号:Go技术栈,ID:GoStack
版权归作者所有,任何形式转载请联系作者。
作者:搜云库技术团队
出处:https://gostack.souyunku.com/...
有疑问加站长微信联系(非本文作者)