Go语言:也许是最简洁版本,一篇文章上手Go语言

Version:1.0StartHTML:000000246EndHTML:000206289StartFragment:000163976EndFragment:000206217StartSelection:000163976EndSelection:000206038SourceURL:https://www.sohu.com/a/327409421_268033?spm=smpc.author.fd-d.2.1564226426023cKH40hi

导读:Go语言是近年来最热门的编程语言,是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go语言同时具备开发效率高和执行效率高两大特点,被誉为云计算时代的C语言。本文作者通过一篇文章带你学会Go语言。

Go 语言是一门开源语言,能够轻松的构建简单,可靠,高效的软件。—— Golang

在很多语言中,解决给定的问题通常有多种方式。工程师需要花费大量的时间思考什么才是解决问题的最优解法。而在Golang中,问题的解法通常只有一种。

这一特性大大节约了工程师的时间,而且使得维护大型代码库变得更容易。在Golang中没有maps和filter这样"高消耗"的特性。

语言的特性带来更好的表现力也带来代价。

——Rob Pike

1. 开始动手

Golang由包组成。Golang编译器将main包编译为可执行文件,而非共享库。main包是应用的入口,通常被定义如下:

packagemain

下面看一个hello world 的例子,在Golang 的工作空间创建main.go文件。

1.1 工作空间

在Go语言中,工作空间由环境变量GOPATH定义。所有编写的代码需要在工作空间中。Go语言会在GOPATH和GOROOT的路径中搜索包。GoROOT是在安装的时候确定的安装路径。

下面来设置GOPATH,我们将~/workspace 加入工作空间。

# export envexportGOPATH=~/workspace# go inside the workspacecd~/workspace

1.2 HELLO WORLD!

我们在刚才的工作空间下创建main.go 文件,代码如下:

package mainimport("fmt")funcmain{fmt.Println("Hello World!")}

上面的例子中,fmt是Go内置的格式化I/O函数。

我们在Go语言中使用import关键字导入包,func main 是入口函数。Println是fmt包中函数,用于打印 "Hello World!"。

让我们开始运行该文件。众所周知Go是编译型语言,我们在运行之前先进行编译。

>gobuild main.go

这将会创建一个二进制运行文件main,我们现在来运行它:

>./main#Hello World!

另一种简单的方式是使用go run 命令:

gorun main.go# Hello World!

2. 变量

Go中的变量类型是显式指定的。Go语言是强类型语言,这意味着在变量声明的时候会检查变量类型。

变量定义如下所示:

varaint

在这个例子中,a的初始值被设置为0。用下面的方式可以定义并初始化变量。

vara =1

这里的变量被编译器推断为int。更简单的变量定义如下所示:

message :="hello world"

我们也可以在同一行声明多个变量:

varb, cint=2,3

3.数据类型 3.1 NUMBER,STRING, BOOLEAN

int 的类型有 int, int8, int16, int32, int64, unit, unit8, unit16, unit 32, unit64, unitptr...

String类型使用byte序列存储数据,用关键字string来声明变量。

bool 关键字表示布尔类型。

Golang 也支持复数,用conplex64和complex128表示。

varabool=truevat bint=1varcstring="hello world"vardfloat32=1.222varxcomplex128= cmplx.Sqrt(-5+12i)

3.2 ARRAYS,SLICES,MAPS

Array 是同类型元素的数组。Array在声明的时候会指定长度且不能改变。一个数组的定义如下:

vara[5]int

也有多维数组,定义如下

var multiD [2][3]int

Slices 是能随时扩容的同类型元素的序列 。Slice的声明方式如下:

varb []int

这将会创建一个容量为0,长度为0的Slice。Slice也可以定义容量和长度,格式如下:

numbers :=make([]int,5,10)

这个Slice初始长度为5,容量为10。

Slice是数组的封装,其内部实现是数组,slice有三个元素,容量,长度和指向内部数组的指针。

Slice的容量可以通过append 或者 copy函数增加。Append函数也能在数组的末尾添加元素,在容量不足的情况下会对slice扩容。

numbers= append(numbers,1,2,3,4)

另一种增加slice容量的方式是使用copy函数。Copy函数的原理是创建一个新的大容量的slice,并把原有的slice拷贝到新的slice中。

// 创建新的slicenumber2 :=make([]int,15)// 复制原有的slice到新的slicecopy(number2, number)

我们也可以创建slice的子slice。例子如下:

packagemainimport("fmt")

funcmain{// 初始化slicenumber2 := []int{1,2,3,4}fmt.Println(number2)// -> [1 2 3 4]// 创建子sliceslice1 := number2[2:]fmt.Println(slice1)// -> [3 4]slice2 := number2[:3]fmt.Println(slice2)// -> [1 2 3]slice3 := number2[1:4]fmt.Println(slice3)// -> [2 3 4]}

Go语言中的Map是键值对,定义如下:

varmmap[string]int

m是定义的变量名,键的类型是string,值的类型是integers。Map中添加键值对的例子如下:

packagemainimport("fmt")

funcmain{m :=make(map[string]int)// 添加键值对m["clearity"] =2m["simplicity"] =3// 打印值fmt.Println(m["clearity"])// -> 2fmt.Println(m["simplicity"])// -> 3}

4. 类型转换

使用类型转换能够改变数据类型,例子如下:

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{a :=1.1b :=int(a)fmt.Println(b)//-> 1}

5. 条件表达式 5.1 IF ELSE

If else 的例子如下,需要注意的是花括号和条件表达式位于同一行。

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{ifnum :=9; num <0{fmt.Println(num,"is negative")}elseifnum <10{fmt.Println(num,"has 1 digit")}else{fmt.Println(num,"has multiple digits")}}

5.2 SWITCH CASE

Switch case 能组织多条件表达式,例子如下:

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{i :=2switchi {case1:fmt.Println("one")case2:fmt.Println("two")default:fmt.Println("none")}}

6. 循环

Golang中只有一个循环表达的关键字,不同形式的循环表达式如下:

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{i :=0sum :=0fori <10{sum +=1i++}fmt.Println(sum)}

上面的例子和C语言中的while循环类似,更为正式的循环表达形式如下:

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{sum :=0fori :=0; i <10; i++ {sum += i}fmt.Println(sum)}

Go 语言中的死循环定义如下:

for{}

7. 指针

Go 语言可以使用指针,指针存储变量的地址,指针用*来定义。指针的定义和所指数据的类型相关:

varap *int

这里的ap是指向整型数据的指针,&用于获取所变量的地址。

a :=12ap = &a

* 用于获取指针所指的地址的值。

fmt.Println(*ap)// => 12

指针通常用于将结构体做为参数传递。

传值通常意味着拷贝,意味着需要更多的内存。

使用指针传递时,在函数中改变的变量会传递给调用的方法或函数。

packagemainimport("fmt")

funcincrement(i *int){*i++}

funcmain{i :=10increment(&i)fmt.Println(i)}

//=> 11

8. 函数

main 包中main函数是golang 程序的入口。我们可以定义多个函数并调用。例如:

packagemainimport("fmt")

funcadd(aint, bint)int{c := a + breturnc}

funcmain{fmt.Println(add(2,1))}//=> 3

从上面的例子中我们可以看出,Golang 中的函数用func关键字加上函数名, 后面是附带数据类型的参数,最后是函数的返回类型。

函数的返回值可以被预先定义,例子如下:

packagemainimport("fmt")

funcadd(aint, bint)(cint){c = a + breturn}

funcmain{fmt.Println(add(2,1))}//=> 3

这里c定义为返回值,因此变量c将会被自动返回,无需在函数最后的return中声明。

你也可以定义一个多个返回值的函数,使用,进行分割。

packagemainimport("fmt")

funcadd(aint, bint)(int,string){c := a + breturnc,"successfully added"}

funcmain{sum, message := add(2,1)fmt.Println(message)fmt.Println(sum)}

//=> successfully added//=> 3

8. 方法,结构体,接口

Golang 不是完全的面向对象语言,但是支持很多面向对象的特性,例如有结构体,接口,方法等。

8.1 结构体

结构体是有类型,不同变量的集合。例如我们想定义Person类型,其中包含姓名,年龄,性别。例如:

type personstruct{name Stringageintgenderstring}

定义好了person结构体后,我们现在来使用它:

//方式 1: 指定属性和值p = person{name:"Bob",age:42,gender:"Male"}//方式 2: 只指定值person{"Bob",42,"Male"}

我们可以使用.符号访问这些属性:

p.name//=> Bobp.age//=> 42p.gender//=> Male

你也可是使用指针访问结构体的属性:

pp = &person{name:"Bob", age: 42, gender:"Male"}pp.name//=> Bob

8.2 方法

方法是一种带有接受器的特殊函数。接收器可以是值或者指针。例子如下:

packagemainimport"fmt"

// 定义结构体typepersonstruct{namestringageintgenderstring}

// 定义方法func(p *person)describe{fmt.Printf("%v is %v years old.", p.name, p.age)}

func(p *person)setAge(ageint){p.age = age}

func(p person)setName(namestring){p.name = name}

funcmain{pp := &person{name:"Bob", age:42, gender:"Male"}pp.describe// => Bob is 42 years oldpp.setAge(45)fmt.Println(pp.age)//=> 45pp.setName("Hari")fmt.Println(pp.name)//=> Bob}

从上面的例子我们可以看出,使用.操作符调用方法,例如pp.describe。需要注意的是,接收器是指针的话,我们传递的是值的引用,这意味着我们在方法做修改将会反映到变量pp上。该不会创建对象的拷贝,将会节省内存。

从上面的例子我们可以看出,age的值被改变了,而name的值并没有改变。这是因为方法setName的接受器不是指针。

8.3 接口

Golang中的接口是方法的集合,接口有助于将同类型的属性组合起来,让我们一起来看一个anminal的接口。

typeanimalinterface{deionstring}

这里的animal是接口类型,我们来创建两种类型的animal并实现接口。

packagemainimport("fmt")

typeanimalinterface{deionstring}

typecatstruct{TypestringSoundstring}

typesnakestruct{TypestringPoisonousbool}

func(s snake)deionstring{returnfmt.Sprintf("Poisonous: %v", s.Poisonous)}

func(c cat)deionstring{returnfmt.Sprintf("Sound: %v", c.Sound)}

funcmain{vara animala = snake{Poisonous:true}fmt.Println(a.deion)a = cat{Sound:"Meow!!!"}fmt.Println(a.deion)}

//=> Poisonous: true//=> Sound: Meow!!!

在main函数中,我们创建可一个animal类型的变量a。我们把 snake和cat类型赋值给animal,使用Println 输出a.deion。

我们在cat和snake中使用不同的方式实现了describe方法,我们得到了不同类型的输出。

9.包

在Golang中,我们的代码在某个包下。main包是程序执行的入口。在Go中有很多内置的包,例如我们之前用过的fmt包。

Go 的包机制是大型软件的基础,能够将大型的工程分解成小部分。

—— Robert Griesemer

9.1 安装一个包

goget// 例子goget github.com/satori/go.uuid

安装的包保存在GOPATH的环境中,你可以在 $GOPATH/pkg 路径下看到安装的包。

9.2 创建一个自定义包

首先创建一个文件夹 custom_package:

>mkdir custom_package>cdcustom_package

创建自定义包的第一步是创建一个和包名相同的文件夹。我们要创建person包,因此我们在custom_package文件下创建person文件夹:

>mkdir person>cdperson

在该路径下创建一个文件person.go:

packagepersonfuncDeion(namestring)string{return"The person name is: "+ name}

funcsecretName(namestring)string{return"Do not share"}

现在我们来安装这个包,这样我们就可以导入和使用它了:

>go install

接下来我们返回custom_package 文件夹中,创建一个main.go:

packagemainimport("custom_package/person""fmt")

funcmain{p := person.Deion("Milap")fmt.Println(p)}

// => The person name is: Milap

在这里,我们可以导入之前创建的包person,需要注意的是在person包中函数secretName不能被访问,这是因为Go中小写字母开头的函数是私有函数。

9.3 生成包文档

Golang中有内置的功能支持包文档。运行下面的命令将生成文档:

godocperson Deion

这将会为Deion 函数生成文档,想要在web服务器上查看文档需要运行下面的命令:

godoc-http=":8080"

现在打开链接http://localhost:8080/pkg/将会看到我们刚才看到的文档。

9.4 Go 中内置的包 9.4.1 fmt

fmt包实现可标准的I/O函数,我们在之前的包中用过其中的打印输出函数。

9.4.2 json

Golang中另一个内置的重要包的是json,它能够对JSON进行编解码。

编码

packagemainimport("encoding/json""fmt")

funcmain{mapA :=map[string]int{"apple":5,"lettuce":7}mapB, _ := json.Marshal(mapA)fmt.Println(string(mapB))}

解码

packagemainimport("encoding/json""fmt")

typeresponsestruct{PageNumberintjson:"page"Fruits []stringjson:"fruits"}

funcmain{str := {"page":1,"fruits": ["apple","peach"]}res := response{}json.Unmarshal([]byte(str), &res)fmt.Println(res.PageNumber)}

//=> 1

解码的时候使用Unmarshal方法,第一个参数是json字节,第二个参数是要映射的结构体的地址。需要注意的是json中的“page”对应的是结构体中的PageNumber。

10. 错误处理

错误是程序中不应该出现的结果。假设我们编写一个API调用外部的服务。这个API可能成功也可能失败。当存在错误是,Golang程序能够识别:

resp, err := http.Get("http://example.com/")

对API的 调用可能成功也可能失败,我们可以通过检查错误是否为空来选择处理方式。

packagemainimport("fmt""net/http")

funcmain{resp, err := http.Get("http://example.com/")iferr !=nil{fmt.Println(err)return}fmt.Println(resp)}

10.1 从函数中返回自定义错误

当我们在自定义函数是,某些情况下会产生错误。我们可以使用error 对象返回这些错误:

packagemainimport("errors""fmt")

funcIncrement(nint)(int, error){ifn <0{// return error objectreturn0, errors.New("math: cannot process negative number")}return(n +1),nil}

funcmain{num :=5ifinc, err := Increment(num); err !=nil{fmt.Printf("Failed Number: %v, error message: %v", num, err)}else{fmt.Printf("Incremented Number: %v", inc)}}

// => The person name is: Milap

Go 内置的包,外部的包都有处理错误的机制。因此我们调用的函数都有可能产生错误。这些错误不应该忽略而是应该向上面的例子那样被优雅的处理。

10.2 Panic

Panic是程序运行中突然产生未经处理的异常。在Go中,panic不是合理处理异常的方式,推荐使用error对象代替。当Panic产生时,程序将会暂停运行。当panic被defer之后,程序才能继续运行。

//Gopackagemainimport"fmt"

funcmain{ffmt.Println("Returned normally from f.")}

funcf{deferfunc{ifr :=recover; r !=nil{fmt.Println("Recovered in f", r)}}fmt.Println("Calling g.")g(0)fmt.Println("Returned normally from g.")}

funcg(iint){ifi >3{fmt.Println("Panicking!")panic(fmt.Sprintf("%v", i))}deferfmt.Println("Defer in g", i)fmt.Println("Printing in g", i)g(i +1)}

10.3 Defer

Defer 在函数结尾一定会执行。在上面的函数中,使用panic 暂停程序的运行,defer语句使程序执行结束时使用改行。Defer也可以用作我们想要在函数的结尾执行的语句,例如关闭文件。

11. 并发

Golong使用轻量级线程Go routies支持并发。

11.1 Go routine

Go routine 是能够并行运行的函数。创建Go routine 非常简单,只需要在函数前添加关键字go,这样函数就能够并行运行了。Go routines 是轻量级的,我们能够创建上千个Go routines。例如:

package mainimport("fmt""time")

funcmain{gocfmt.Println("I am main")time.Sleep(time.Second*2)}

funcc{time.Sleep(time.Second*2)fmt.Println("I am concurrent")}

//=> I am main//=> I am concurrent

上面的例子中,函数c是Go routine,能够并行运行。我们想要在多线程中共享资源,但是Golang并不支持。因为这会导致死锁和资源等待。Go 提供了另一种共享资源的方式:channel。

11.2 Channels

我们可以使用Channel在两个Go routine之间传递数据。创建channel之前需要制定接受的数据类型。例如我们创建了一个接受string类型的channel。

c :=make(chanstring)

有了这个channel之后,我们可以通过这个channel发送和接收string类型的数据。

packagemainimport"fmt"

funcmain{c :=make(chanstring)gofunc{ c <-"hello"}msg := <-cfmt.Println(msg)}

//=>"hello"

接收的channel一直等待发送的channel发送数据。

11.3 One way channel

有些情况下,我们希望Go routine 通过channel接收数据,但不发送数据,反之亦然。这时候我们可以创建一个one-way channel。例如:

packagemainimport("fmt")

funcmain{ch :=make(chanstring)gosc(ch)fmt.Println(<-ch)}

funcsc(chchan<-string){ch <-"hello"}

上面例子中,sc是一个Go routine只能给channel发送数据而不能接受数据。

12. 使用selecct优化多个channel

有这样一种情况,一个函数等待多个channel,这时候我们可以使用select语句。例如:

packagemainimport("fmt""time")

funcmain{c1 :=make(chanstring)c2 :=make(chanstring)gospeed1(c1)gospeed2(c2)fmt.Println("The first to arrive is:")select{cases1 := <-c1:fmt.Println(s1)cases2 := <-c2:fmt.Println(s2)}}

funcspeed1(chchanstring){time.Sleep(2* time.Second)ch <-"speed 1"}

funcspeed2(chchanstring){time.Sleep(1* time.Second)ch <-"speed 2"}

上面例子中,main函数等待两个channel,c1和c2。使用select语句,先从channel中收到的数据会被打印出来。

12.1 Buffered channel

在Golang中可以创建buffered channel,当buffer满的时候,发送数据给channel将会被阻塞。例如:

packagemainimport"fmt"

funcmain{ch :=make(chanstring,2)ch <-"hello"ch <-"world"ch <-"!"// extra message in bufferfmt.Println(<-ch)}

// => fatal error: all goroutines are asleep - deadlock!

Golang为什么如此成功?

简单。。。—— Rob-pike

13. 总结

我们学习Golang以 下的主要模块和特性:

变量,数据类型

Array,Slices和Map

函数

循环和条件语句

指针

方法,结构体和接口

错误处理

并发——Go routines和channels

恭喜你,你已经对Go有了很好的理解。

One of my most productive days was throwing away 1,000 lines of code.

—— Ken Thompson

不要停下脚步,继续前进。思考一个小应用程序并开始动手。

原文链接:

https://milapneupane.com.np/2019/07/06/learning-golang-from-zero-to-hero/

本文作者Milap Neupane,由何朋朋翻译。转载本文请注明出处,欢迎更多小伙伴加入翻译及投稿文章的行列,详情请戳公众号菜单「联系我们」。

技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容