5

鉴于上篇文章我们已经讲过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语法规定

  1. 每个Go文件的第一行(不包含注释)都必须是package的定义
  2. 可执行的程序package定义必须是main
  3. 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
print 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/...


架构师专栏
6.2k 声望7k 粉丝