Go语言基础入门
- windows 下的开发IDE
- 直接用jetbrains教育网账号激活
- 基本数据类型
- 标识符(identifiers)
- 关键字(keywords)
- 标点符号(punctuation)
- 字面量(literals)
- 左位移与iota计数配合实现存储单位的常量枚举
- Go语言约定规则
- 注释
windows 下的开发IDE
GoLand
直接用jetbrains教育网账号激活
基本数据类型
(a) 指针类型(Pointer) (b) 数组类型 (c) 结构类型(struct) (d) Channel 类型 (e) 函数类型 (f) 切片类型 (g) 接口类型(interface) (h) Map 类型
标识符(identifiers)
- 下划线也被认为是字母
关键字(keywords)
break default func interface select case
defer go map package switch const
fallthrough if range type
continue for import return var
变量声明使用关键字var
例
1 | var( |
一个变量var 声明之后,系统自动赋予它该类型的零值
下面是各种变量以及其对应的零值
- int 0
- float 0.0
- bool false
- string 空字符串
- 指针 nil
多变量可以在同一行进行赋值,称为并行赋值
例1
2
3
4
5
6a,b,c = 5, 7 ,"abc"
简式声明:
a,b,c := 5,7,"abc"
简式声明一般用在func内,注意 全局变量和简式声明的变量不要同名,否则容易产生偶然的变量隐藏
反例
1 | func main() { |
交换变量不用再用交换函数了
a,b = b,a
空白标识符常常用来抛弃值
例:
1 | _,b= 5,7 |
标点符号(punctuation)
字面量(literals)
####################################################
左位移与iota计数配合实现存储单位的常量枚举
1 | type Bytesize float64; |
Go语言约定规则
可见性规则
标识符必须以一个大写字母开头,这样才可以被外部包的代码所使用,这被称为导出。标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的。但是包名不管在什么情况下都必须小写
命名规范和语法惯例
- 命名
当某个函数需要被外部包调用的时候需要使用大写字母开头,并遵循 Pascal 命名法(“大驼峰式命名法”);否则就遵循“小驼峰式命名法”,即第一个单词的首字母小写,其余单词的首字母大写。
单词之间不以空格断开或连接号(-)、底线(_)连结,第一个单词首字母采用大写字母;后续单词的首字母亦用大写字母,例如:FirstName、LastName。每一个单词的首字母都采用大写字母的命名格式,被称为“Pascal命名法”,源自于Pascal语言的命名惯例,也有人称之为“大驼峰式命名法”(Upper Camel Case),为驼峰式大小写的子集。
当二个或二个以上单词连结在一起时,用驼峰式命名法可以增加变量和函数名称的可读性。
- 分号的使用也使用分号作为语句的结束,但一般会省略分号。像在标识符后面;整数、浮点、复数、Rune或字符串等字面量后面;关键字break、continue、fallthrough、或者return后面;操作符或标点符号++、–、)、]或}之后等等都可以使用分号,但是往往会省略掉,像LiteIDE编辑器会在保存.go文件时自动过滤掉这些分号,所以在Go语言开发中一般不用过多关注分号的使用。
- 左大括号 { 不能单独一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示“ expected declaration, found ‘{‘ ”。右大括号 } 需要单独一行。
- 在定义接口名时也有惯例,一般单方法接口由方法名称加上-er后缀来命名
注释
- 行注释,使用双斜线//开始,一般后面紧跟一个空格。行注释是Go语言中最常见的注释形式,在标准包中,一般都采用行注释,建议采用这种方式。
- 块注释:使用 /* */,块注释不能嵌套。块注释一般用于包描述或注释成块的代码片段。
注释文字尽量每行长度接近一致,过长的行应该换行以方便在编辑器阅读。注释可以是单行,多行,甚至可以使用doc.go文件来专门保存包注释。每个包只需要在一个go文件的package关键字上面注释,两者之间没有空行。
对于变量,函数,结构体,接口等的注释直接加在声明前,注释与声明之间没有空行。
- 函数或方法的注释需要以函数名开始,且两者之间没有空行
- 在方法,结构体或者包注释前面加上“Deprecated:”表示不建议使用
在注释中,还可以插入空行
代码结构化
包的概念
Go语言中包的主要作用是把功能相似或相关的代码组织在同一个包中,以方便查找和使用
每个.go文件都必须归属于某一个包,每个文件都可有init()函数。包名在源文件中第一行通过关键字package指定,包名要小写
包的导入
一个Go程序通过import关键字将一组包链接在一起。import其实是导入目录,而不是定义的包名称,实际应用中我们一般都会保持一致
当导入多个包时,一般按照字母顺序排列包名称,像LiteIDE会在保存文件时自动完成这个动作。所谓导入包即等同于包含了这个包的所有的代码对象。
为避免名称冲突,同一包中所有对象的标识符必须要求唯一。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
三种特殊的import方式
点操作的含义是某个包导入之后,在调用这个包的函数时,可以省略前缀的包名,如这里可以写成Println(“Hello World!”),而不是fmt.Println(“Hello World!”)。例如:
import( . “fmt” )
别名操作
别名操作就是可以把包命名成另一个容易记忆的名字。例如:
import(
f “fmt”
)
别名操作调用包函数时,前缀变成了别名,即f.Println(“Hello World!”)。在实际项目中有时这样使用,但请谨慎使用,不要不加节制地采用这种形式。
引入包但是不直接使用包里的函数,只调用该包里面的init函数
_ 操作是引入某个包,但不直接使用包里的函数,而是调用该包里面的init函数,比如下面的mysql包的导入。此外在开发中,由于某种原因某个原来导入的包现在不再使用,也可以采用这种方式处理,比如下面fmt的包。代码示例如下:
import (
_ “fmt”
_ “github.com/go-sql-driver/mysql”
)
标准库
在 Go 的安装文件里包含了一些可以直接使用的标准库。在GOROOT/src中可以看到源码,也可以根据情况自行重新编译。
完整列表可以访问GoWalker(https://gowalker.org/)查看。
unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
1 | syscall-os-os/exec: |
导入外部安装包
go install
可以使用go install在你的本地机器上安装它们。go install 是Go语言中自动包安装工具:如需要将包安装到本地它会从远端仓库下载包:检出、编译和安装一气呵成。
go install/build都是用来编译包和其依赖的包。
go install 使用了 GOPATH 变量
假设你想使用https://github.com/gocolly/colly 这种托管在 Google Code、GitHub 和 Launchpad 等代码网站上的包。
你可以通过如下命令安装: go install github.com/gocolly/colly 将一个名为 github.com/gocolly/colly 安装在GOPATH/pkg/ 目录下。
区别: go build只对main包有效,在当前目录编译生成一个可执行的二进制文件(依赖包生成的静态库文件放在GOPATH/pkg)。
go install一般生成静态库文件放在GOPATH/pkg目录下,文件扩展名a。
包的init()初始化
可执行应用程序的初始化和执行都起始于main包。如果main包的源代码中没有包含main()函数,则会引发构建错误 undefined: main.main。main()函数既没有参数,也没有返回类型,init()函数和main()函数在这一点上两者一样
等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init()函数,最后执行main()函数。
Go语言中init()函数常用于包的初始化
init()函数的特征
init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
每个包可以拥有多个init函数
包的每个源文件也可以拥有多个init函数
同一个包中多个init()函数的执行顺序不定
不同包的init()函数按照包导入的依赖关系决定该函数的执行顺序
init()函数不能被其他函数调用,其在main函数执行之前,自动被调用
项目结构
使用godoc
godoc工具会收集这些注释并产生一个技术文档
命令行下进入目录下并输入命令: godoc -http=:6060 -goroot=”.”
然后在浏览器打开地址:http://localhost:6060
然后你会看到本地的 Godoc 页面,从左到右一次显示出目录中的包
Go程序的编译
编译有关的命令主要是go run ,go build , go install这三个命令
go run只能作用于main包文件,先运行compile 命令编译生成.a文件,然后 link 生成最终可执行文件并运行程序,这过程的产生的是临时文件,在go run 退出前会删除这些临时文件(含.a文件和可执行文件)。最后直接在命令行输出程序执行结果。go run 命令在第二次执行的时候,如果发现导入的代码包没有发生变化,那么 go run 不会再次编译这个导入的代码包,直接进行链接生成最终可执行文件并运行程序
go install用于编译并安装指定的代码包及它们的依赖包,并且将编译后生成的可执行文件放到 bin 目录下(GOPATH/bin),编译后的包文件放到当前工作区的 pkg 的平台相关目录下。
go build用于编译指定的代码包以及它们的依赖包。如果用来编译非main包的源码,则只做检查性的编译,而不会输出任何结果文件。如果是一个可执行程序的源码(即是 main 包),这个过程与go run 大体相同,除了会在当前目录生成一个可执行文件外。
使用go build时有一个地方需要注意,对外发布编译文件如果不希望被人看到源代码,请使用go build -ldflags 命令,设置编译参数-ldflags “-w -s” 再编译后发布。避免使用gdb来调试而清楚看到源代码
运算符
^ 运算符表示的是按位异或
&& 逻辑与
& 按位与
左移n位表示乘以2的n次方,高位丢弃,低位补零
&^将运算符左边数据相异的位保留,相同位清除
string
Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串
1 | func main() { |
字符串拼接
- 由于编译器行尾自动补全分号的缘故,加号 + 必须放在第一行。 拼接的简写形式 += 也可以用于字符串
- strings.Join()
1 | strings.Join([]string{"hello", "world"}, ", ") |
Join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是本来没有,去构造这个数据的代价也不小。
- bytes.Buffer
1 | var buffer bytes.Buffer |
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 capacity。
- string.Builder
1
2
3
4
5var b1 strings.Builder
b1.WriteString("ABC")
b1.WriteString("DEF")
fmt.Print(b1.String())
strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。strings.Builder 同样也提供了 Grow() 来支持预定义容量。当我们可以预定义我们需要使用的容量时,strings.Builder 就能避免扩容而创建新的 slice 了。strings.Builder是非线程安全,性能上和 bytes.Buffer 相差无几。
里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给 GC 带来额外的负担,所以性能比较差。
有关string处理的包
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。
strings 包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到
slice切片
切片(slice) 是对底层数组一个连续片段的引用,所以切片是一个引用类型。切片提供对该数组中编号的元素序列的访问。未初始化切片的值为nil。
与数组一样,切片是可索引的并且具有长度。切片s的长度可以通过内置函数len() 获取;与数组不同,切片的长度可能在执行期间发生变化。元素可以通过整数索引0到len(s)-1来寻址。我们可以把切片看成是一个长度可变的数组。
切片提供了计算容量的函数 cap() ,可以测量切片最大长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说,这个不等式永远成立:0 <= len(s) <= cap(s)。
一旦初始化,切片始终与保存其元素的基础数组相关联。因此,切片会和与其拥有同一基础数组的其他切片共享存储;相比之下,不同的数组总是代表不同的存储。
切片下面的数组可以延伸超过切片的末端。容量是切片长度与切片之外的数组长度的总和。
使用内置函数make()可以给切片初始化,该函数指定切片类型和指定长度和可选容量的参数
切片相较于数组的优点
因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用
切片格式 var identifier []type,一个切片在未初始化之前默认是nil,长度为0
切片的初始化格式
1 | var slice1 []type = arr1[start:end] |
类似数组初始化的方式
1 | var x = []int{2,3,5,7,9} |
make()函数创建切片,同时创建相关数组:
1 | var slice1 []type = make([]type,len,cap) |
简写为
1 | v:=make([]int,10,50) |
slice 重组
slice1= slice2[0:end]
copy()函数避免空间的浪费
1 | package main |
append()追加
append()函数将 0 个或多个具有相同类型S的元素追加到切片s后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果s的容量不足以存储新增元素,append()会分配新的切片来保证已有切片元素和新增元素的存储。
因此,append()函数返回的切片可能已经指向一个不同的相关数组了。append()函数总是返回成功,除非系统内存耗尽了
append()函数操作如果导致分配新的切片来保证已有切片元素和新增元素的存储,也就是返回的切片可能已经指向一个不同的相关数组了,那么新的切片已经和原来切片没有任何关系,即使修改了数据也不会同步。
append()函数操作后,有没有生成新的切片需要看原有切片的容量是否足够
最重要的是看一下底层数组是否还是原来的底层数组
字典map
1 | var map1 map[keytype]valuetype |
在声明的时候不需要知道 map 的长度,map 是可以动态增长的
map 可以用 {key1: val1, key2: val2} 的描述方法来初始化,就像数组和结构体一样
map 容量: 和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。
1 | package main |
会发生错误:
1 | package main |
map 的一些初始化操作
1 | // 声明但未初始化map,此时是map的零值状态 |
map中key值的访问
1 | val1,isPresent := map1[key1] |
if_,ok :=X[“two”];!ok{
fmt.Println(“no entry”)
}
1 | ## map的删除操作 |
package main
import “fmt”
func main(){
data := []int{1,3,2}
for_,v :=range data{
v*=10
}
fmt.Println(“data”,data)//元数据并不会更新
}
1 |
|
package main
import “fmt”
func main(){
data := [] int {1,2,3}
for i,_:=range data{
data[i]*=10
}
fmt.Println(“data”,data)//data [10,20.30]
}
1 |
|
switch var1{
case val1:
‘’’’’
case val2:
‘’’’’
default:
‘’’’’
switch {
case condition1:
…
case condition2:
…
default:
…
}
}
1 | switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true) |
switch initialization {
case val1:
…
case val2:
…
default:
…
}
switch result := calculate(); {
case result < 0:
…
case result > 0:
…
default:
// 0
}
1 | switch 的第三中形式中包含一个初始化的语句 |
package main
import (
“fmt”
“time”
)
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf(“received “, i1, “ from c1\n”)
case c2 <- i2:
fmt.Printf(“sent “, i2, “ to c2\n”)
case i3, ok := (<-c3):
if ok {
fmt.Printf(“received “, i3, “ from c3\n”)
} else {
fmt.Printf(“c3 is closed\n”)
}
case <-time.After(time.Second * 3): //超时退出
fmt.Println(“request time out”)
}
}
1 | ## for循环 |
for i,j := 0,N;i <j;i,j = i+1,j-1{}
1 | 条件语句可以被省略,但是 |
for ix,val := reange coll{}
1 | 特别注意的是 |
package main
import (
“fmt”
“time”
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{ {“one”}, {“two”}, {“three”} }
for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
// goroutines (可能)显示: three, three, three
}
1 | ## 当前的迭代变量作为匿名goroutine的参数 |
package main
import (
“fmt”
“time”
)
func main() {
data := []string{“one”, “two”, “three”}
for _, v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}
time.Sleep(3 * time.Second)
// goroutines输出: one, two, three
}
1 | ### Unicode编码的字符串,可以用for range来进行迭代 |
for pos ,char := range str{
}
1 | if语句由布尔表达式后跟一个或多个语句组成 |
err := errors.New(“math - square root of negative number”)
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New (“math - square root of negative number”)
}
}
1 | 也可以使用fmt创建错误对象 |
if f < 0 {
return 0, fmt.Errorf(“square root of negative number %g”, f)
}
1 | ## panic |
package main
import (
“fmt”
)
func div(a, b int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到异常:%s\n", r)
}
}()
if b < 0 {
panic("除数需要大于0")
}
fmt.Println("余数为:", a/b)
}
func main() {
// 捕捉内部的异常
div(10, 0)
// 捕捉主动的异常
div(10, -1)
}
程序输出:
捕获到异常:runtime error: integer divide by zero
捕获到异常:除数需要大于0
1 | ## Recover()异常恢复 |
func protect(g func()) {
defer func() {
log.Println(“done”)
// 即使有panic,Println也正常执行。
if err := recover(); err != nil {
log.Printf(“run time panic: %v”, err)
}
}()
log.Println(“start”)
g() // 可能发生运行时错误的地方
}
1 | ### defer使用的三个规则 |
package main
import(
“fmt”
“time”
)
func main(){
defer timeCost(time.Now())
fmt.Println(“start program”)
time.Sleep(5*time.Second)
fmt.Println(“finish program”)
}
func timeCost(start time.Time){
terminal:=time.Since(start)
fmt.Println(terminal)
}
1 |
|
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf(“longCalculation took this amount of time: %s\n”, delta)
1 | # 函数 |
func IndexRune(s string, r rune) int {//int 就是返回值的类型
for i, c := range s {
if c == r {
return i
}
}
return // 必须要有终止语句,如果这里没有return,则会编译错误:missing return at end of function
}
1 | **在 Go 语言里面函数重载是不被允许的** |
type funcType func (int, int) int
1 | ### 函数可以给表达式赋值 |
f := func() int{return 7}
1 | 函数类型的定义和调用 |
package main
import (
“fmt”
“time”
)
type funcType func(time.Time) // 定义函数类型funcType
func main() {
f := func(t time.Time) time.Time { return t } // 方式一:直接赋值给变量
fmt.Println(f(time.Now()))
var timer funcType = CurrentTime // 方式二:定义函数类型funcType变量timer
timer(time.Now())
funcType(CurrentTime)(time.Now()) // 先把CurrentTime函数转为funcType类型,然后传入参数调用
// 这种处理方式在Go 中比较常见
}
func CurrentTime(start time.Time) {
fmt.Println(start)
}
1 | ## 函数调用 |
package main
import (
“fmt”
)
// 变参函数,参数不定长
func list(nums …int) {
fmt.Println(nums)
}
func main() {
// 常规调用,参数可以多个
list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 在参数同类型时,可以组成slice使用 parms... 进行参数传递
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
list(numbers...) // slice时使用
}
1 |
|
complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT
1 | ### 函数的递归与回调 |
package main
import “fmt”
// Factorial函数递归调用
func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result
}
return 1
}
// Fac2函数循环计算
func Fac2(n uint64) (result uint64) {
result = 1
var un uint64 = 1
for i := un; i <= n; i++ {
result *= i
}
return
}
func main() {
var i uint64= 7
fmt.Printf(“%d 的阶乘是 %d\n”, i, Factorial(i))
fmt.Printf(“%d 的阶乘是 %d\n”, i, Fac2(i))
}
1 | Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性,这些函数的声明顺序可以是任意的。 |
package main
import(
“fmt”
)
func main(){
callback(1,Add)
}
func Add(a,b int){
fmt.Printf(“%d + %d = %d \n”,a,b,a+b)
}
func callback(y int ,f func(int,int)){
f(y,2)
}
1 | ### 匿名函数 |
func(x, y int) int { return x + y }
1 | 这样的函数不能够独立存在,但可以被赋值于某个变量,即保存函数的地址到变量中: |
package main
import “fmt”
var G int = 7
func main() {
// 影响全局变量G,代码块状态持续
y := func() int {
fmt.Printf(“G: %d, G的地址:%p\n”, G, &G)
G += 1
return G
}
fmt.Println(y(), y)
fmt.Println(y(), y)
fmt.Println(y(), y) //y的地址
// 影响全局变量G,注意z的匿名函数是直接执行,所以结果不变
z := func() int {
G += 1
return G
}()
fmt.Println(z, &z)
fmt.Println(z, &z)
fmt.Println(z, &z)
// 影响外层(自由)变量i,代码块状态持续
var f = N()
fmt.Println(f(1), &f)
fmt.Println(f(1), &f)
fmt.Println(f(1), &f)
var f1 = N()
fmt.Println(f1(1), &f1)
}
func N() func(int) int {
var i int
return func(d int) int {
fmt.Printf(“i: %d, i的地址:%p\n”, i, &i)
i += d
return i
}
}
程序输出:
G: 7, G的地址:0x54b1e8
8 0x490340
G: 8, G的地址:0x54b1e8
9 0x490340
G: 9, G的地址:0x54b1e8
10 0x490340
11 0xc0000500c8
11 0xc0000500c8
11 0xc0000500c8
i: 0, i的地址:0xc0000500e8
1 0xc000078020
i: 1, i的地址:0xc0000500e8
2 0xc000078020
i: 2, i的地址:0xc0000500e8
3 0xc000078020
i: 0, i的地址:0xc000050118
1 0xc000078028
1 | ## 变参函数 |
package main
import “fmt”
func Greeting(who …string) {
for k, v := range who {
fmt.Println(k, v)
}
}
func main() {
s := []string{“James”, “Jasmine”}
Greeting(s…) // 注意这里切片s… ,把切片打散传入,与s具有相同底层数组的值。
}
程序输出:
0 James
1 Jasmine
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 583614868@qq.com
文章标题:Go语言基础入门
文章字数:9.9k
本文作者:钟帅豪
发布时间:2019-11-16, 17:07:53
最后更新:2019-11-22, 17:30:53
原始链接:http://jhshz520.github.io/2019/11/16/Go语言基础入门/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。