【跟着我们学Golang】基础结构

鉴于上篇文章我们已经讲过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个。

breakcasechanconstcontinue
defaultdeferelsefallthroughfor
funcgogotoifimport
interfacemappackagerangereturn
selectstructswitchtypevar

当然,除了关键字,Go还保留了一部分基本类型的名称、内置函数的名称作为标识符,共计36个。

appendboolbytecapclosecomplex
complex64complex128copyfalsefloat32float64
imagintint8int16int32int64
iotalenmakenewnilpanic
printprintlnrealrecoverstringtrue
uint16uint32uint64uintuint8uintptr

另外,_下划线也是一个特殊的标识符,被称为空白标识符。所以,他可以像其他标识符那样接收变量的声明和赋值。但他的作用比较特殊,用来丢弃那些不想要的赋值,所以,使用_下划线来声明的值,在后续的代码中是无法使用的,当然也不能再付给其他值,也不能进行计算。这些变量也统一被称为匿名变量。

总结

到这里,本篇内容讲解了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'获取。

微信公众号

【跟着我们学Golang】基础结构

首发微信公众号:Go技术栈,ID:GoStack

版权归作者所有,任何形式转载请联系作者。

作者:搜云库技术团队
出处:https://gostack.souyunku.com/...

相关推荐