一.go程序相对路径,执行区别;
依赖相对路径的文件,出现路径出错的问题:
go run 和 go build 不一样,一个到临时目录下执行,一个可手动在编译后的目录下执行,路径的处理方式会不同不断go run,不断产生新的临时文件。因为 go run 和 go build 的编译文件执行路径并不同,执行的层级也有可能不一样,自然而然就出现各种读取不到的问题。
解决方法:
1.配置公有环境变量法。
2.传递参数法。
二.GO环境变量含义
GOROOT:go环境安装路径
GOPATH:
- go环境安装/go get 跟go工具都会使用到GOPATH环境变量,另外GOPATH还作为编译后的二进制存放目录和import包是的搜索路径
- GOPATH下有三个目录,bin,pkg,src
- bin:存放编译的可执行文件
- pkg:存放编译好的库文件,主要是*.a文件
- src:主要存放go的源文件
GO111MODULE:go环境是否使用module模式开关(go11之后使用module模式管理)
GOPROXY:主要是用于设置Go模块代理
配置环境变量如下:
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=/home/admin/go
export PATH=$PATH:$GOPATH/bin
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
// https://proxy.golang.org,direct,该代理国内无法访问
// 所以使用命令go env -w GOPROXY=https://goproxy.cn,direct(该命令默认的位置:/root/.config/go/env)
三.Go学习redis协议
协议格式:
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
......
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
例:
*3
$3
set
$3
$cls
$3
pwd
协议的实际展示:
"*3\r\n$3\r\nset\r\n$3\r\ncls\r\n$3\r\npwd\r\n"
如下程序:
package main
import (
"fmt"
"log"
"net"
"os"
"github.com/redis-protoc/protocol"
)
const (
Address = "127.0.0.1:6379"
Network = "tcp"
)
func Conn(network, address string) (net.Conn, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
return conn, nil
}
func main() {
// 读取入参
args := os.Args[1:]
fmt.Println("args = ", args)
if len(args) <= 0 {
log.Fatalf("Os.Args <= 0")
}
// redis协议的参数格式:
// *<参数数量> CR LF
// $<参数1 的字节数量> CR LF
// <参数1 的数据> CR LF
// ...
// $<参数 N 的字节数量> CR LF
// <参数 N 的数据> CR LF
|---------------------------|
// 例如:
// *3
// $3
// SET
// $5
// mykey
// $7
// myvalue
// 获取请求协议
reqCommand := protocol.GetRequest(args)
// 连接 Redis 服务器
redisConn, err := Conn(Network, Address)
if err != nil {
log.Fatalf("Conn err: %v", err)
}
defer redisConn.Close()
// 写入请求内容
_, err = redisConn.Write(reqCommand)
if err != nil {
log.Fatalf("Conn Write err: %v", err)
}
// 读取回复
command := make([]byte, 1024)
n, err := redisConn.Read(command)
if err != nil {
log.Fatalf("Conn Read err: %v", err)
}
fmt.Println("command = ", string(command))
// 处理回复
reply, err := protocol.GetReply(command[:n])
if err != nil {
log.Fatalf("protocol.GetReply err: %v", err)
}
// 处理后的回复内容
log.Printf("Reply: %v", reply)
// 原始的回复内容
log.Printf("Command: %v", string(command[:n]))
}
发送协议,执行成功如下:
[root@goRedisProtoc] # go run redis_protoc.go set name cler
args = [set name cler]
command = +OK
2021/07/25 16:22:09 Reply: OK
2021/07/25 16:22:09 Command: +OK
命令执行失败:
命令执行失败的时候,返回-1
[root@goRedisProtoc] # go run redis_protoc.go get nvr
args = [get nvr]
command = $-1
2021/07/25 16:23:57 Reply: <nil>
2021/07/25 16:23:57 Command: $-1
组装协议:
func GetRequest(args []string) []byte {
req := []string{
"*" + strconv.Itoa(len(args)),
}
for _, arg := range args {
req = append(req, "$"+strconv.Itoa(len(arg)))
req = append(req, arg)
}
str := strings.Join(req, "\r\n")
return []byte(str + "\r\n")
}
服务返回的状态字节流第一个字符:
const (
StatusReply = '+'
ErrorReply = '-'
IntegerReply = ':'
BulkReply = '$'
MultiBulkReply = '*'
OkReply = "OK"
PongReply = "PONG"
)
=========================
字节流返回的第一个字符表示含义:
状态回复(status reply)的第一个字节是 "+"
错误回复(error reply)的第一个字节是 "-"
整数回复(integer reply)的第一个字节是 ":"
批量回复(bulk reply)的第一个字节是 "$"
多条批量回复(multi bulk reply)的第一个字节是 "*"
四.Go内存分配-逃逸分析
1.概念定义:
所谓的逃逸分析,就是程序编译阶段确定指针动态范围的方法,简单来说就是程序在哪里可以访问到指针,通俗说编译器决定变量的分配分配到堆上还是栈上的过程。
2.特点:
a.是否有在其他地方(非局部)被引用,只要可能被引用了,那么变量就分配到堆上。
b.有些变量即便是没用被外部引用,但是有可能会因为变量太大,也会分配到堆上,y因为栈太小了,无法存放。
3.逃逸分析的背景
a.如果所有的变量都分配堆上,会因不断的申请,释放内存,不仅会影响程序的性能(相对于栈),还会产生内存碎片,除此之外,垃圾回收机制压力增大,从而导致程序的性能不高。
所以,需要使用一种按需分配,按需选择的方案,在特定情况,合适的地方分配内存在对应的位置上才能合理的利用内存资源,从而保证系统的可行性&高可用性。
4.发生逃逸分析的几种示例:
{
-m: 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一. 般用 1 个就可以了
-l:会禁用函数内联,在这里禁用掉 inline 能更好的观察逃逸情况,减少干扰
}
方案1-指针
a.外部引用结构
package main
import "fmt"
type UserInfo struct {
Name string
}
func GetUserInfo() *UserInfo {
return &UserInfo{}
}
func main() {
_ = GetUserInfo()
}
root@goBasicGrammer % go build -gcflags '-m -l' yace.go
# command-line-arguments
./yace.go:8:9: &UserInfo literal escapes to heap
如上所示,表示对应的UserInfo被外部引用,所以内存分配到堆上。其实不难想象,虽然在GetUserInfo方法中,局不变量,如果分配在栈上,函数返回后,自动回收了,那就完犊子了,所以
想一想分配的内存也应该在堆上,毕竟也是被外部引用了。
方案2-未确定类型
a.局部指针分配在栈上
package main
func main() {
str := new(string)
*str = "zhang"
}
root@ goBasicGrammer % go build -gcflags '-m -l' yace.go
# command-line-arguments
./yace.go:12:12: new(string) does not escape
如上所示,str虽然是指针,实际上变量分配到栈上,没有被作用于外部引用。
如果加上fmt.Println(*str),就会出现变化,如下代码:
package main
func main() {
str := new(string)
*str = "zhang"
fmt.Println(*str);
}
root@ goBasicGrammer % go build -gcflags '-m -l' yace.go
# command-line-arguments
./yace.go:14:12: new(string) does not escape
./yace.go:17:13: ... argument does not escape
./yace.go:17:14: *str escapes to heap
如上所示17:14,此时str分配到堆上,为什么使用了一个fmt就会出现这种变量分配转移呢?
原因: func Println(a …interface{}) (n int, err error) 接收参数是一个interface类型的,程序在编辑阶段无法确定变量的具体类型,因此会发生逃逸分析,从而会将变量分配到堆上。
方案3-泄漏参数
a.参数传递指针
package main
type UserInfo struct {
Name string
}
func GetUserInfo(u *UserInfo) *UserInfo {
return u
}
func main() {
_ = GetUserInfo(&UserInfo{Name: "erl"})
}
root@ goBasicGrammer % go build -gcflags '-m -l' yace.go
# command-line-arguments
./yace.go:7:18: leaking param: u to result ~r1 level=0
./yace.go:11:18: &UserInfo literal does not escape
如上,内存分配到栈上,虽然调用了GetUserInfo,传递的是指针,什么都没做,就直接返回了传递的原来的指针,等价于没有进行任何引用,还在main的作用域中,所以分配到了栈上。
更改一下,传遍非指针变量
package main
type UserInfo struct {
Name string
}
func GetUserInfo(u UserInfo) *UserInfo {
return &u
}
func main() {
_ = GetUserInfo(UserInfo{Name: "erl"})
}
root@ goBasicGrammer % go build -gcflags '-m -l' yace.go
# command-line-arguments
./yace.go:7:18: moved to heap: u
如上,参数传递变成了,变量,其实就等价于方案1中结构一样,因为在main调用方法GetUserInfo,传递了参数,方法参数会产生一份拷贝,如果该参数分配在栈上,那么函数返回,该参数就被回收了,外部的引用就完犊子了,所有此时会产生逃逸分析,将结构分配到堆上。
五.golang中init方法
1.init方法会在致执行main函数之前执行。
2.init函数执行的顺序依赖于包依赖的顺序来执行
3.一个包中可以存在多个init函数,并且多个init函数依次执行。
4.包的每个源文件可以有多个init函数同时存在。按照init函数定义顺序执行。
{
func init() {
fmt.Println("first init")
}
func init() {
fmt.Println("second init")
}
func init() {
fmt.Println("third init")
}
func main() {
}
}
六,go中的标志位运算
通常用于标志判断.
package main
import (
"fmt"
)
var (
first = 0x01
second = 0x02
third = 0x04
)
func main() {
flag := 0
// 添加标志位
flag |= first
flag |= second
flag |= third
// 判断标志位
fmt.Println(flag)
fmt.Println(flag & first)
fmt.Println(flag & second)
fmt.Println(flag & third)
// 删除标志位置
flag = (flag & ^first) // 某一位取反,在进行按位与
fmt.Println(flag)
flag = (flag & ^second)
fmt.Println(flag)
}
七,go中的别名定义
如下代码:
package main
import (
"fmt"
"reflect"
)
// type Enu int32 // 创建了一种新的自定义类型,其底层类型是int32 // 输出:[ Enu - int32 - 3 ]
type Enu = int32 // 创建int32 类型的别名 输出:[ int32 - int32 - 3 ]
func main() {
var a Enu = 3
ret := reflect.TypeOf(a)
fmt.Println(ret.Name(), "-", ret.Kind(), "-", a)
}
八,golang对epoll封装特性总结
1.整体封装一个Listen方法,其内部包含socket的创建,绑定,监听,epoll_create,epoll_ctl将socked添加到对应的epoll结构上.
2.实现的Accept方法,当没有链接到来的时候,系统内部的底层调用回返回EAGAIN,并且将挡当前的协程阻塞,反之当有链接到来,将其新生成的socked添加到epoll中管理,再返回.
3.再Read方法的处理,当没有数据到来时,内部系统调用返回一个EAGAIN,也将自己阻塞,阻塞处理跟2中的一样.同样Write函数也是如此,只是Write再写入数据,发现缓冲区不够,就先讲自己阻塞起来,等到缓冲区足够了,再继续写入相关数据,阻塞跟2中也一样的代码处理.
4.协程唤醒,再2,3中的处理当中,当协程阻塞之后,如何来被唤醒继续处理运行呢?再系统的内部有一个系统将空sysmon,该监控协程会调用netpoll持续性的调用epoll_wait来查看epoll对象管理的文件描述符释放就绪状态.如果有就绪文件描述符,那么就直接唤醒其协程继续执行。
以上的模型,虽看应用层看似同步,实质上再底层,通过协程&epoll配合管理,避免线程的切换性能损耗,故而并不会阻塞用户线程,替代的是开销更小的协程。
九,go中值接受跟指针结构特点
1.值对象跟指针对象分别调用值接收跟指针接收方法
type Person struct {
Name string
Age int
}
func (p *Person) GetName() string {
return p.Name
}
func (p Person) GetAge() int {
return p.Age
}
func main() {
p := Person{
"lx",
10,
}
fmt.Println("p= ", p.GetName(), "+", p.GetAge())
t := &Person{
"lx",
10,
}
fmt.Println("t= ", t.GetName(), "+", t.GetAge())
}
结果:
p= lx + 10
t= lx + 10
如上通过结果可以看到两个成员分别使用了不同的接收方式进行处理。main中使用值接收对象进行调用方法,正常。反过来,通过创建一个指针对象t调用方法,也能正常调用.
2.接口接收调用其方法
package main
import "fmt"
type World interface {
GetName() string
GetAge() int
}
type Person struct {
Name string
Age int
}
func (p *Person) GetName() string {
return p.Name
}
func (p Person) GetAge() int {
return p.Age
}
func main() {
p := Person{
"lx",
10,
}
fmt.Println("p= ", p.GetName(), "+", p.GetAge())
t := &Person{
"lx",
10,
}
fmt.Println("t= ", t.GetName(), "+", t.GetAge())
var w World
w = Person{ // 当前方法创建一个值对象赋予接口对象w,调其实现的方法的时候,报错如下结果中提示。
"www", 1000,
}
fmt.Println("w= ", w.GetName(), "+", w.GetAge())
//w = &Person{ // 该出使用指针传递方法,可以调用值接收跟指针接收方式。运行正常。
// "www",
// 1000,
//}
//
//fmt.Println("w= ", w.GetName(), "+", w.GetAge())
}
结果:
cannot use Person{…} (value of type Person) as type World in assignment:
Person does not implement World (GetName method has pointer receiver)
以上出现的结果原因:
使用值接收:(type)meath(),系统内部隐式会自定义一个(*type) meath()
使用指针接收: (*type) meath(),系统不会创建隐式自定义一个(type) meath()
通过上述结论得到结果:两种方式不要混用,容易造成一些未知的问题,实际使用中,根据实际情况,统一一种方式使用,避免出现不必要的错误。建议还是使用指针接收。因为相关操作变更跟其本身都会变更。
十 go中for循环采坑记录
代码如下:
package main
import "fmt"
func main(){
var t []int
a := []int{1,2,3,4}
for _,v := range a {
t = append(t,v)
}
fmt.Println(t)
}
结果:
[root@localhost go]# go run test.go
[1 2 3 4]
上输代码变量v,使用append,存储的一份拷贝,所以切片t中能达到预期效果输出。
*如果上代码修改 []int 为 []*int,如下代码所示:
package main
import "fmt"
func main(){
var t []*int
a := []int{1,2,3,4}
for _,v := range a {
t = append(t,&v)
}
fmt.Println(t)
}
[root@localhost go]# go run test.go
[0xc0000180b0 0xc0000180b0 0xc0000180b0 0xc0000180b0]
4
4
4
4
如上结果发现,数据全部都是4,未能达到期望。同事也看到t中存储的都是同一个地址,乃是最后一个元素。
原因:每次进行循环,&v都是取得变量v内存地址,从切片a中每次循环都是将值存放到了变量v当中,这样t中每次appen存储都是变量v的地址,每次循环都是将值存储至变量v的内存中,故而出现上述结果。
如想得到预期的结果,修改代码如下:
package main
import "fmt"
func main(){
var t []*int
a := []int{1,2,3,4}
for _,v := range a {
v := v // 修改该处,使用都是一个新的创建的变量,如下结果变量地址也不同,所以存储的结果也不一样
t = append(t,&v)
}
fmt.Println(t)
for _,v := range t {
fmt.Println(*v)
}
}
[root@localhost go]# go run test.go
[0xc0000180b0 0xc0000180b8 0xc0000180c0 0xc0000180c8]
1
2
3
4
再append之前使用一个临时变量,每次都是一个新的创建的变量,如下结果变量地址也不同,所以存储的结果也不一样,达到预期效果。
再如下代码:
package main
import "fmt"
func main(){
var prints []func()
var d []int
for _, v := range []int{1, 2, 3,4} {
//v := v //使用一个临时变量,存储每个函数中都进行自己的一份拷贝
//prints = append(prints, func() { fmt.Println(v) }) 1 2 3
fmt.Println(&v)
prints = append(prints, func() { fmt.Println(v) })
/*
4 4 4 4 //v的变量,所有的值最后都被访问的为是4,通过如下结果,在Println中存储是变量v的地址,这样导致
每次循环都是将新值存储至变量v中,当循环结束时,当前v中存储就是最后一个值,但是每个func中存储都是v的变量地址,
所以调用func函数时,访问的都是变量&v中的值,所以全部都是4.
*/
d = append(d,v) // 存储是是v具体值得拷贝,所以能正常预期访问结果
}
fmt.Println("---")
for _, print := range prints {
print()
}
fmt.Println("---")
for _,v := range d {
fmt.Println(v)
}
}
[root@localhost go]# go run test.go
0xc0000180b0
0xc0000180b0
0xc0000180b0
0xc0000180b0
---
4
4
4
4
---
1
2
3
4
搜查一些文档中解释,这种处理,后续通过go.mod的版本控制处理,可能不同版本出现结果稍微有点偏差。
**
十一.利用同一平台编译构建不同系统的运行程序
该点具体需要配置两个参数,如下,且通过改变两个参数,能分别编译出不同平台的可执行程序进行运行。
export GOOS=linux
export GOARCH=amd64