主要参考对象
/zh-cn/learn/paths/go-first-steps/
/
安装
/
下载安装即可
语言
工作区
Go 在组织项目文件方面与其他编程语言不同。 首先,Go 是在工作区的概念下工作的。工作区就是应用程序源代码所在的位置。 所有 Go 项目共享同一工作区,可以通过环境变量$GOPATH 修改。go 1.11 引入了 Go Modules模块,环境变量GO111MODULE = on时(不同版本含义有点不一样)可以将所有的代码仓库均不存储在 GOPATH 下。
在任意一个目录下执行 go mod init 会生成文件,之后就可以直接执行这个目录下的文件(类似这个目录就成了工作区)
常见目录结构
- bin
- src
- pkg
变量
使用var 声明变量
在没有值的情况下需要标注类型
var firstName string
var firstName, lastName string
var (
firstName, lastName string
age int
)
初始化变量
如果你决定初始化某个变量,则不需要指定其类型,因为当你使用具体值初始化该变量时,Go 会推断出其类型
使用冒号等于号时,要声明的变量必须是新变量,在声明函数外的变量时,必须使用 var 关键字执行此操作,即:=只能在函数内部使用,如果声明全局变量需要使用var关键字
如果声明了变量但未使用,Go 会抛出错误, 使用特殊字符 _ 可以占位,不使用,让编译顺利通过(一些函数会返回多个值,必须接收,但又没用)
var (
firstName = "John"
lastName = "Doe"
age = 32
)
var (
firstName, lastName, age = "John", "Doe", 32
)
func main() {
firstName, lastName := "John", "Doe"
age := 32
(firstName, lastName, age)
}
常量
const HTTPStatusOK = 200
const (
StatusOK = 0
StatusConnectionReset = 1
StatusOtherError = 2
)
数据类型
Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值.共有四类数据类型:
- 基本类型:数字、字符串和布尔值
- 聚合类型:数组和结构
- 引用类型:指针、切片、映射、函数和通道
- 接口类型:接口
在 Go 中,如果你不对变量初始化,所有数据类型都有默认值。 此功能非常方便,因为在使用之前,你无需检查变量是否已初始化。
基本类型
- 数字
- 字符串
- 布尔值
数字
int8、int16、int32(rune)、int64、uint8(byte)、uint16、uint32、uint64、float32、float64
默认值: 整数数值为0,浮点数为+0.000000e+000
定义整数类型的关键字是 int,但 Go 还提供了 int8、int16、int32 和 int64 类型,其大小分别为 8、16、32 或 64 位的整数。 使用 32 位操作系统时,如果只是使用 int,则大小通常为 32 位。 在 64 位系统上,int 大小通常为 64 位。 但是,此行为可能因计算机而不同。 可以使用 uint。 但是,只有在出于某种原因需要将值表示为无符号数字的情况下,才使用此类型。 此外,Go 还提供 uint8、uint16、uint32 和 uint64 类型。
Go 语言把字符分 byte 和 rune 两种类型处理。 rune 是int32 的别名,常用于中文的对应的Unicode, byte 是uint8的别名,/cheyunhua/p/
如果尝试在不同类型之间执行数学运算,将会出现错误, 在 Go 中将值从一种类型转换为另一种类型时,需要显式声明新类型
布尔值
true、false
默认值: false
你可以使用关键字 bool 声明布尔类型。 Go 不同于其他编程语言,在 Go 中,你不能将布尔类型隐式转换为 0 或 1。 你必须显式执行此操作
字符串
string 用于表示字符串数据类型。 若要初始化字符串变量,你需要在双引号(")中定义值。 单引号(')用于单个字符
默认值: string类型空值
类型转化
使用内置函数
var integer16 int16 = 127
var integer32 int32 = 32767
(int32(integer16) + integer32)
使用 strconv 包
package main
import (
"fmt"
"strconv"
)
func main() {
i, _ := ("-42")
s := (-42)
(i, s)
}
…
函数
函数包括一般的函数,以及特殊函数(方法):在函数名称之前加入一个额外的参数,此附加参数称为接收方。
方法的用处:分组函数并将其绑定到自定义类型(例如:结构)。 Go 中的这一方法类似于在其他编程语言中创建类,因为它允许你实现面向对象编程 (OOP) 模型中的某些功能,例如嵌入、重载和封装。
函数
main()
Go 中的所有可执行程序都具有此函数,因为它是程序的起点。 你的程序中只能有一个 main() 函数。 如果创建的是 Go 包,则无需编写 main() 函数。
main() 函数没有任何参数,并且不返回任何内容。 如要访问 Go 中的命令行参数,可以使用用于保存传递到程序的所有参数的 os 包 和 变量来执行操作
自定义函数
语法: 关键字(func),自定义命名(name),参数(parameters),返回值(results:可以是函数内部的变量,这样return就不用待参数了;如果只是类型声明,则要明确返回数据;可以多个)
func name(parameters) (results) {
body-content
}
方法
方法使用中的一些注意点:
- 使用指针:方法需要更新变量,或者,如果参数太大,则可能需要避免复制它。在遇到此类情况时,你需要使用指针传递变量的地址。另外依据 Go 的约定,如果结构的任何方法具有指针接收方,则此结构的所有方法都必须具有指针接收方,即使某个方法不需要也是如此。
- 声明其它类型(非结构):可以为任何类型创建方法,但不能给其它包的类型定义方法。因此无法给基本类(例如:string)定义方法。可以在上面加装一层
package main
import (
"fmt"
"strings"
)
type upperstring string
func (s upperstring) Upper() string {
return (string(s))
}
func main() {
s := upperstring("Learning Go!")
(s)
(())
}
- 嵌入方法: 结构(A)中属性可以使用另一个结构(B),如果B存在方法(fc),则示例A可以直接调用该方法,有点类型基类、子类的继承关系。实际上Go 编译器会通过创建新的方法来实现。
func (t A) fc() int {
return ()
}
- 重载方法:上述3中希望A的方法C与B不同,可以直接定义方法。要继续使用B的C方法,只能显性调用()
func (t A) fc() int {
return something
}
- 封装(访问权限): 对象的发送方(客户端)无法访问某个方法。 通常,在其他编程语言中,你会将 private 或 public 关键字放在方法名称之前。 在 Go 中,只需使用大写标识符,即可公开方法,使用非大写的标识符将方法设为私有方法。另外,Go 中的封装仅在程序包之间有效。 换句话说,你只能隐藏来自其他程序包的实现详细信息,而不能隐藏程序包本身。
接口
接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。
接口是一种抽象类型
- 声明接口
type Shape interface {
Perimeter() float64
Area() float64
}
- 实现接口
type Square struct {
size float64
}
func (s Square) Area() float64 {
return *
}
func (s Square) Perimeter() float64 {
return * 4
}
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return * *
}
func (c Circle) Perimeter() float64 {
return 2 * *
}
func printInformation(s Shape) {
("%T\n", s)
("Area: ", ())
("Perimeter:", ())
()
}
func main() {
var s Shape = Square{3}
printInformation(s)
// 此时运行时可以自动转换类型,不报错
c := Circle{6}
printInformation(c)
}
- 实现字符串接口
- 扩展现有实现
包
Go 包与其他编程语言中的库或模块类似。 你可以打包代码,并在其他位置重复使用它。 包的源代码可以分布在多个 .go 文件中。
- 声明: 在文件中添加 package calculator ,声明这个go文件为calculator 包
- 导入: 在 Go 中,包名称需遵循约定。 包使用其导入路径的最后一部分作为名称。 例如,Go 标准库包含名为 math/cmplx 的包,该包提供用于处理复数的有用代码。 此包的导入路径为 math/cmplx
import "math/cmplx"
()
main包
在 Go 中,甚至最直接的程序都是包的一部分。 通常情况下,默认包是 main 包。如果程序是 main 包的一部分,Go 会生成二进制文件。 运行该文件时,它将调用 main() 函数。
换句话说,当你使用 main 包时,程序将生成独立的可执行文件。 但当程序非是 main 包的一部分时,Go 不会生成二进制文件。 它生成包存档文件(扩展名为“.a”的文件)。
一般包
你现在可以开始编写包的函数和变量。 不同于其他编程语言,Go 不会提供 public 或 private 关键字,以指示是否可以从包的内外部调用变量或函数。 但 Go 须遵循以下两个简单规则:
- 如需将某些内容设为专用内容,请以小写字母开始。
- 如需将某些内容设为公共内容,请以大写字母开始。
例如:
package calculator
var logMessage = "[LOG]"
// Version of the calculator
var Version = "1.0"
func internalSum(number int) int {
return number - 1
}
// Sum two integer numbers
func Sum(number1, number2 int) int {
return number1 + number2
}
- 只能从包内调用 logMessage 变量。
- 可以从任何位置访问 Version 变量。 建议你添加注释来描述此变量的用途。 (此描述适用于包的任何用户。)
- 只能从包内调用 internalSum 函数。
- 可以从任何位置访问 Sum 函数。 建议你添加注释来描述此函数的用途。
模块
可以将包放到模块中。 Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。
此外,模块还有助于其他开发人员引用代码的特定版本,并更轻松地处理依赖项。 另一个优点是,我们的程序源代码无需严格存在于 $GOPATH/src 目录中。 如果释放该限制,则可以更方便地在其他项目中同时使用不同包版本。
因此,若要为 calculator 包创建模块,请在根目录 ($GOPATH/src/calculator) 中运行以下命令:go mod init /myuser/calculator
。运行此命令后,/myuser/calculator 就会变成模块的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 的新文件。
导入本地模块时,可以直接使用上述mod init的名称,再去生成的中修改指向
module helloworld
go 1.14
require /myuser/calculator v0.0.0
replace /myuser/calculator => ../calculator
其它
新手错题集
struct literal uses unkeyed fields
出现在结构实例的时候
- 检查结构体是否真的存在该字段
- 检查是否在包外实例结构,但字段名是小写开头(类似私有),需改为大写
missing type in composite literal
常出现在结构实例的时候,特别是嵌套时,偷懒导致
- 结构不能直接{}, 得先声明,类似这样 Structname{},嵌套内的结构也一样,偷懒会忘了
too few values in struct literal
- 如下所示:实例的时候 t 这种写法必须参数写全,不然会报错
type triangle struct {
size int
}
type coloredTriangle struct {
triangle
color string
}
t := coloredTriangle{triangle{3}, "blue"}
t1 := new(coloredTriangle)
= triangle{3}
t2 := coloredTriangle{
triangle: triangle{3},
color: "blue",
}
has no field or method
- 代码类似上面错误里的代码,这里主要是指嵌套结构的调用( Embed methods)时报错,如下所示:、()都会报错,原因在于定义coloredTriangle的triangle给了变量名(上面那个没有),调用的时候必须显性的这样
type triangle struct {
size int
}
type coloredTriangle struct {
a triangle
color string
}
func (t triangle) perimeter() int {
return * 3
}
t := coloredTriangle{triangle{3}, "blue"}
("Size:", )
("Perimeter", ())
cannot assign 1 values to 2 variables 或者 cannot initialize 1 variables with 2 values
- 注意赋值的时候不要少了一个变量接收,特别是调用函数的时候,是有返回多个值的,例如 int, error , 必须两个变量接收
struct 类似类用法,但继承存在问题
如下所示,通过结构的嵌套实现类似class的效果,这种就相当于A为基类,定义一个值(很多方法需要这个值),这个值子类编写的会直接传入(好像没有直接给默认值的法子),可以在B实例后调用New_b来实现默认值
type A struct{
a string
b map[string]string
}
type B Struct {
A
}
var map_b:= {
"test_key": "test_value"
}
func (b B)New_b() B{
= map_b
return b
}
test_b := B{A:{a:"test_a"}}
cannot use a (variable of type any) as string value in argument
泛型使用(any要至少1.18以上): 直接作为函数入参使用变量会报错,得转以下,顺便判断下a, ok := a.(string)
string 字符串多行
- 使用反引号`` , 注意不要乱打空格制表
gkb utf 互转 (enconding)
- 使用官方的第三方包(绝了)
// /golang/text
// go get -u /x/text
import "/x/text/encoding/simplifiedchinese"
mb, _ := ().Bytes(b)
golang与其它语言的比较
python
基于个人想法,受限水平,可能存在错误
- 明显的例如:golang是强类型语言,golang没有类(严格来说并不是一个面向对象的语言,但是可以实现类似的功能,例如通过结构、接口等)
- goruntine 是 go语言的特色,用于并发操作。在代码书写,思考逻辑上,个人感觉更类似python的线程(thread),而不是协程(coroutine)。这两者都是为了实现并发处理。这意味这goruntine更容易编写(个人感觉),而python的coroutine编写需要显性的不停的调用await、 async。
# 线程
import threading
def do_something():
do something ...
do_thread = threading.Thread(target=do_something)
do_thread.start()
do other ...
# 协程corountine
import asyncio
async def do_something():
do something ...
await ...
do ...
asyncio.run(do_something())
python示例如上,咋一看thread和corountine差不多,实践中多corountine并行处理的遇上其中一个阻塞,会导致整体阻塞(协程的本质是自己主动切换执行代码,遇上等待就去执行其它代码),而thread基本不会。
以工作经验举例:开发一个tcp服务用于模拟设备行为
行为1: 发起tcp链接登录
行为2: 登录后发送心跳包
行为3: 登录后等待接收服务器数据,接收数据解析并应答
thread实现:非常简单,使用queue做线程间数据传输,event做一些流程控制。主线程实例socket执行到登录,起三个子线程,一个循环发送queue中的数据,一个定时生成心跳放置到queue中,一个循环接收数据,数据接收到后,将应答放置到queue中。主线程进行循环等待脚本退出信号。出现严重错误,如socket异常后,重新实例socket,销毁所有子线程重头再来。
corountine实现:基本也是类似,但要注意在子线程里阻塞的一些操作,不会影响其它线程,例如是一个阻塞操作(没收到数据会一直等待),我是通过设置超时,让销毁子线程的时候能够一段时间后关闭,而协程里不能直接这么用,阻塞行为会让所有并发协程一起卡住(不太会异步写法,还好及时发现定时发送慢了),当然asyncio库已经带了socket异步化的操作(reader, writer = await asyncio.open_connection(family=socket.AF_INET,sock=mysocket)…当时找了半天)。还有socket异常后的处理,相对线程方案来说也比较麻烦,总而言之,编写的时候需要更加注意。
package main
import (
"fmt"
"time"
)
func time_to_sleep(s int) {
("%d before sleep 当前日期:%s\n", s, ().Format("2006-01-02 15:04:05.000"))
(5 * )
("%d after sleep 当前日期:%s\n", s, ().Format("2006-01-02 15:04:05.000"))
}
func main() {
start := ()
for i := 1; i <= 3; i++ {
go time_to_sleep(i)
}
elapsed := (start)
("Done! It took %v seconds!\n", ())
(10 * )
("after all 当前日期:%s\n", ().Format("2006-01-02 15:04:05.000"))
}
Done! It took 0 seconds!
1 before sleep 当前日期:2022-08-04 16:49:23.643
3 before sleep 当前日期:2022-08-04 16:49:23.643
2 before sleep 当前日期:2022-08-04 16:49:23.643
2 after sleep 当前日期:2022-08-04 16:49:28.665
1 after sleep 当前日期:2022-08-04 16:49:28.665
3 after sleep 当前日期:2022-08-04 16:49:28.665
after all 当前日期:2022-08-04 16:49:33.655
结果输出代码结果输出如上所示,整体类似python的thread效果,还有,注意主线程是不管goruntine的,一旦结束,goruntine会直接结束,所以要等待goruntine完成,这里是直接等待,实际使用可能会使用channel或者 sync 的 WaitGroup