Coder Social home page Coder Social logo

blog's People

Contributors

guoyuefei avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

golang的注意点

golang的注意点


目录


1. 可以返回局部变量的指针

作为少数包含指针的语言,它与C还是有所不同。C中函数不能够返回局部变量的指针,因为函数结束时局部变量就会从栈中释放。而golang可以做到返回局部变量的一点

#include <iostream>
using namespace std;
int* get_some() {
	int a = 1;
	return &a;
}

int main() {
	cout << "a = " << *get_some() << endl;
	return 0;
}

*这个明显在c/c++中是错误的写法,a出栈后什么都没了。 会发生一下错误:

$ g++ t.cpp
> t.cpp: In function 'int* get_some()':
> t.cpp:4:6: warning: address of local variable 'a' > returned [-Wreturn-local-addr]
>   int a = 1;
      ^

go语言试验代码如下:

package main
import "fmt"
func GetSome() *int {
	a := 1;
	return &a;
}

func main() {
	fmt.Printf("a = %d", *GetSome())
}

基本相同的代码,但是有以下运行结果

> $ go run t.go  
> a = 1   

显然不是go的编译器识别不出这个问题,而是在这个问题上做了优化。参考go FAQ的原文:

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

这里的意思就是让我们无需担心返回的指针是空悬指针。我理解的意思是,普通情况下函数中局部变量会存储在堆栈中,但是如果这个局部变量过大的话编译器可能会选择将其存储在堆中,这样会更加有意义。还有一种情况,当编译器无法证明在函数结束后变量不被引用那么就会将变量分配到垃圾收集堆上。总结一句:编译器会进行分析后决定局部变量分配在栈还是堆中

嗯。。。利用这个特性我们可以使用以下方式来达到并发做某事的作用

func SomeFun() <-chan int {
    out := make(chan int)
    go func() {
        //做一些不可告人的事情。。。
    }()
    return out
}

2. Go提供的两种分配原语——内建函数new和make

Go语言提供了两种分配的原语,即内建函数new和make。它们做的事情不同。

  1. new它不会初始化内存,而是将内存置零。也就是说new(T)会为类型T的新项分配一个已置零的内存空间,并返回它的地址,也就是*T。即它会返回一个指针,这个指针是指向这个类型T的零值的那份空间。
  2. make的函数签名make(T, args)。它仅用于切片、map和chan类型的创建。make会直接返回一个类型为T的而非指针,当然这个值是已初始化过的。用法已切片为例,例如:
make([]int, 10, 100)

会分配一个容量为100,长度为10的int类型的切片结构。

new([]int)

这个会返回一个指向新分配得,已置零得切片结构,即指向nil切片值的指针。

下面例子阐明了new和make之间的区别:

var p *[]int = new([]int)       //分配切片结构;*p = nil;基本没用
var v []int = make([]int, 100)  //切片v现在引用了一个具有100个int元素的新数组

//没必要这么麻烦
var p *[]int = new([]int)
*p = make([]int, 100, 100)

//习惯用法
v := make([]int, 100)

记住,make只适用于map、切片和chan且不返回指针。若要获得明确的指针,请使用new分配内存

3. 复合字面

在os标准包中有以下代码,这个函数相当于其他语言中的构造函数

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

这里显得代码显得过于冗长,可以使用复合字面来简化代码

func NewFile(fd int, name string) *File {
	if fd < 0 {
		return nil
	}
	f := File{fd, name, nil, 0}
	return &f
}

由上面的代码可以知道复合字面File{fd, name, nil, 0}返回的是一个量的引用而非指针,所以最后返回时需要取地址符。

4. 基础类型中数组、切片、string、map、chan是值类型还是引用类型

这个是很有必要注意的一件事,用为当你让函数中传入一个数组时,能不能改变外部数值的值呢?这就要考验到数值类型是值类型还是引用类型了。如果是引用类型的话,相当于c语言中传入指针一样,可以在函数内部改变传入参数的外部的值,但是如果是值类型的话,在传入函数过程中只是将一份拷贝传入,故不可在函数内部修改外部的值。 string为值类型, 其类型维护了一个不安全指针和长度两个量。
go语言中的数组是值类型的,这与其他语言大不一样,拿c/c++为例:

#include <iostream>
using namespace std;
const int NUM = 5;
int a[NUM] = {5,4,3,2,1};

void change_a(int arr[],int n) {
        for(int i = 0; i < n; i++){
                arr[i]--;
        }
}

int main() {
        change_a(a,NUM);
        for(int i = 0; i < NUM; i++) {
                cout << "a[" << i << "] = " << a[i] << endl;
        }
}

结果如下:

Administrator@PC-201809211459 MINGW64 ~/Desktop
$ g++ v.cpp -o v.exe

Administrator@PC-201809211459 MINGW64 ~/Desktop
$ ./v.exe
a[0] = 4
a[1] = 3
a[2] = 2
a[3] = 1
a[4] = 0

很显然,函数内部改变了形参数组导致全局变量a数组发生了改变

下面是golang的代码

package main

import "fmt"

var a [5]int = [5]int{5,4,3,2,1}

func changeA(arr [5]int) {
        for i := 0; i < 5; i++ {
                arr[i]--
        }
}

func main() {
        changeA(a)
        for i,v := range a {
                fmt.Println("a[",i,"] = ",v)
        }
}

结果如下:

Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run v.go
a[ 0 ] =  5
a[ 1 ] =  4
a[ 2 ] =  3
a[ 3 ] =  2
a[ 4 ] =  1

从此可以看出go语言中的数组是值类型。事实上我们很少用数组去传参数,因为在 go中如果用数组传参的话需要在函数的参数形式列表中写死数组的大小,而这种情况在c/c++中是不需要的。
但是go中传参可以使用切片,因为切片是引用类型的。同上例子如下:

package main

import "fmt"

func main() {
        a := []int{5,4,3,2,1}
        func(arr []int) {
                for i := 0; i < len(arr); i++ {
                        arr[i]--
                }
        }(a)
        for i,v := range a {
                fmt.Println("a[",i,"] = ",v)
        }
}

结果如下:

Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run vv.go
a[ 0 ] =  4
a[ 1 ] =  3
a[ 2 ] =  2
a[ 3 ] =  1
a[ 4 ] =  0

由此可见golang的数组是值类型的,但是切片是引用类型的。
记下 []T{}、map、chan作为基础系统类型里的三个引用类型,而且这三个都是可以使用make这个内联函数的。第七点会归纳这部分内联函数

5. 初始化函数init

这个函数比较神奇啊,我看官方文档的时候有些看不懂.官方文档(镜像网站上的中文官方文档)的一段

最后,每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (其实每个文件都可以拥有多个 init 函数。)而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而那些 init 只有在所有已导入的包都被初始化后才会被求值。

除了那些不能被表示成声明的初始化外,init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。

我在试验了之后大概得出结论,在import某个包的会执行该包下所有文件的init函数,执行顺序与文件在文件系统的排序有关。
vv.go,main函数所在文件

package main

import(
	"fmt"
	"./some"
	_ "./another"
)

func init() {
	fmt.Println("hello")
}

func main() {
	a := []int{5,4,3,2,1}
	some.ChangeA(a)
	for i,v := range a {
		fmt.Println("a[",i,"] = ",v)
    }
	var c int = 10
	fmt.Println(c)
}

package some下有三个文件
some0.go

package some
import "fmt"
func init() {
	fmt.Println("some0")
}

some1.go

package some
import (
	"fmt"
)
var a int
func init(){
	a = 10
	fmt.Println("package some init done!",a)
}
func ChangeA(arr []int) {
	for i := 0; i < len(arr); i++ {
		arr[i]--
	}
}

some2.go

package some
import "fmt"
func init() {
	fmt.Println("some2")
}

package another下一个文件
another.go

package another
import "fmt"
func init() {
	fmt.Println("package another init done!")
}

以上代码为了节省空间,某些为了美观的空行省略了。
最后运行结果如下:

some0
package some init done! 10
some2
package another init done!
hello
a[ 0 ] =  4
a[ 1 ] =  3
a[ 2 ] =  2
a[ 3 ] =  1
a[ 4 ] =  0
10

假如将another.go的文件小做修改,修改如下:

package another
import "fmt"
import _ "../some"
func init() {
	fmt.Println("package another init done!")
}

得到的结果不变,可见,init函数只会执行一遍,而不是碰到import它所在的包就执行。

6. 关于指针与值

这个我也是比较糊的,所以在这里进行了部分整理和试验。估计以后还有更多关于这点的问题
先把重要点记下:

  1. 绑定在类型指针*T上的方法可以改变该类型的值,但是只绑定在类型T上的方法是无法改变该类型的值的。有以下代码:
package main

import "fmt"

type Si int

func (s *Si)Plus1(a Si) {
	*s += a
}

func (s Si)Plus2(a Si) {
	s += a
}

func main() {
	var s Si = 10
	s.Plus1(1)
	fmt.Println("after Plus1: s = ",s)
	s.Plus2(1)
	fmt.Println("after Plus2: s = ",s)
}

运行结构如下:

after Plus1: s =  11
after Plus2: s =  11

可见Plus2并没有发挥其作用。
go语言是一门一眼就能看得懂的语言,其他语言中把成员函数神奇的封装在一个类里,但go不是,函数在前面的小括号里写的参数就是指定了我这个函数是归属于哪个类型的,而且显式的将该类型的值传入函数了,也就是函数名前面的括号其实就可以看作是形参列表
2. 类型向接口赋值的时候应该取地址

package main

import "fmt"

type Si int

type Plus interface {
	Plus1(a Si)
	Plus2(a Si)
}

func (s *Si)Plus1(a Si) {
	*s += a
}

func (s Si)Plus2(a Si) {
	s += a
}

func main() {
    var s Si = 10
	var ss Plus = &s
	ss.Plus1(1)
	fmt.Println("after Plus1: s = ",s)
}

以上代码是成立并且是能正确运行的
但是如果将上面的var ss Plus = &s变成var ss Plus = s就会出现编译错误。该编译错误如下:

# command-line-arguments
cmd\tt.go:26:6: cannot use s (type Si) as type Plus in assignment:
	Si does not implement Plus (Plus1 method has pointer receiver)

这个编译错误提示很有意思啊,前半段提醒我们并没有实现Plus接口,我们可能会认为Si类型明明实现了Plus接口啊。这是怎么回事呢?其实看括号里的话结合最上面未出错的程序就会明白,其实编译器的意思就是*Si实现了接口Plus但是Si并没有实现。
为什么会出现这种情况呢,其实是因为接口有一个函数绑定在指针上func (s *Si)Plus1(a Si),而Si类型是没有实现这个函数的,故没有实现Plus接口。可是为什么func (s Si)Plus1(a Si)绑定在Si上但是*Si也实现了Plus接口。那是因为go编译器可以自动根据func (s Si)Plus1(s Si)这个函数生成func (s *Si)Plus1(a Si),故而*Si实现了所有函数。
当然以上自动生成的过程反过来是无法实现的,因为指针的权限大的原因,func (s *Si)Plus1(a Si)可能会改变s的值,而func (s Si)Plus1(a Si)无法做到,故而编译器也不会自动生成。

通过以上分析,我们以以下例子做试验:

package main

import "fmt"

type Si int

type Plus interface {
	Plus1(a Si)
	Plus2(a Si)
}

func (s Si)Plus1(a Si) {
	s += a
}

func (s Si)Plus2(a Si) {
	s += a
}

func main() {
	var s Si = 10
	var ss Plus = s
	ss.Plus1(1)
	fmt.Println("after Plus1: s = ",s)
}

运行结果:
after Plus1: s =  10

为什么是10在第一点已有介绍了。编译通过,第二点分析合理!

  1. 根据以上例子发现了一个奇怪的但又不奇怪的现象。不论是*T还是T都可以直接调用函数。而且在对接口赋值时接口声明部分无需声明为指针也不能声明为指针。接口赋值好后,不能取内容,虽然有些接口看上去是一个指针。
    这里不再举例
    考虑到以上三点,我觉得自己有必要养成的几个习惯:
    1.接口赋值时应该最好使用类型指针对其赋值
    2.写成员函数时遵循最小权限原则,注意*T和T的区别
    3.在调用成员函数时无论是指针还是类型本身都可以直接调用
    4.接口在调用成员函数时就直接调用

7. 切片、map和chan有关的内联函数

这一部分也是比较乱的一点,关联这三个基本类型的内建函数大致可以分成三类。分别与创建、删除、操作。

  1. 创建:map、slice、channel的创建一般都是用make函数来进行内存分配。
  2. 删除:delete主要用于map中删除实例。嗯,channel的close也放在这项中吧。
  3. 操作:len、cap可用于不同的类型,len可用于string、slice、array的长度。cap一般返回slice的分配空间的大小。copy用于复制slice。append用于追加slice.
    ps:new用于各种类型的内存分配不止以上几种。
*具体用法如下*:
  1. make
    1.1. channel: 这里只拿常用类型int做例子:
ch1 := make(chan int)        //不带缓存的channel
ch2 := make(chan int, 1024)     //带缓存的channel
1.2. slice: 针对slice的函数签名make([]type,len)和make([]type,len,cap)
slice1 := make([]int, 10)		//slice1中有10个初始值为零值的元素
slice2 := make([]int, 10, 100)	//slice2中有10个初始值为零的元素,且初始容量为100
1.3. map:签名make(map[keyType]valueType)
mp := make(map[string]int)
mp["啊啊啊"] = 3
  1. append
    2.1. slice: append(slice []Type, elems ...Type) []Type
    append函数主要用于向slice的末尾添加元素的,作为一个特殊的存在可以在字节切片【】byte("hello")中添加字符串string。它会返回一个被更新过的slice,如果要使用它就需要一个变量接收这个更新的值。例子如下:
slice1 = append(slice1,2,3,4)
slice2 = append(slice2,slice1...)
slice3 := append([]byte("hello"),"world"...)
  1. copy
    3.1. slice: copy(dst, src []Type) int
    这个函数需要小心的一点,slice1和slice2两个长度分别为5和3.还是用代码表示吧。。。
//len(slice1)是5
//len(slice2)是3
//i==3,只会复制slice1的前三个元素到slice2中
i := copy(slice2,slice1)
//i==3,只会将slice2中的前三个元素复制到slice1中
i = copy(slice1,slice2)
  1. len、cap
    4.1. len用于获取切片和map长度,channel未取元素个数,cap用于获取切片的容量和channel的缓冲容量。签名:len(v Type) int,cap(v Type) int
len(ch1)
len(slice1)
len(mp)
cap(slice1)
cap(ch1)

len不只用于这三个数据类型,还包括string、数组和指向数组的指针。源代码注释如下:

// The len built-in function returns the length of v, according to its type:
// Array: the number of elements in v.
// Pointer to array: the number of elements in *v (even if v is nil).
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
// String: the number of bytes in v.
// Channel: the number of elements queued (unread) in the channel buffer;
// if v is nil, len(v) is zero.

cap虽然也可以用在数组和指向数据的指针但是其返回内容与len函数相同。源代注释如下:

// The cap built-in function returns the capacity of v, according to its type:
// Array: the number of elements in v (same as len(v)).
// Pointer to array: the number of elements in *v (same as len(v)).
// Slice: the maximum length the slice can reach when resliced;
// if v is nil, cap(v) is zero.
// Channel: the channel buffer capacity, in units of elements;
// if v is nil, cap(v) is zero.

  1. delete
    5.1. delete 只用于map删除元素。签名:delete(m map[Type]Type1, key Type)
delete(mp,"啊啊啊")
  1. close
    6.1. channel可以接受和发送数据,也可以被关闭。当channel关闭后向channel发送数据的操作会引起panic。但是当channel关闭后,我们还能向其中取数据,若是之前的数据还没有取完那么还可以将这些数据取出。当缓存的数据全部取完后,仍然可以对channel取数据,此时的数据为零值数据。签名:close(c chan<- Type)
close(ch)

8. append的故事

append会返回一个切片,需要一个变量接收这个切片。

  1. append会在原slice底层数组容量时继续使用该数组, 这种情况是返回切片不是原切片,但是顶层数组用的是原切片的数组。
  2. append会在原slice底层数组容量不够时更换底层数组,这种情况是返回切片不是原切片,顶层数组也不是原切片的数组。
package main

import "fmt"

func main() {
	x := []int{0,1,2}
	x = append(x, 3)		//
	
    
    a := append(x, 4)
    b := append(x, 5)
	
	// ouput x is [0 1 2 3]
	fmt.Println("x is", x)
    //output a is [0 1 2 3 5]
	fmt.Println("a is", a)
    //output b is [0 1 2 3 5]
	fmt.Println("b is", b)
	
}

如果要得到独立于原切片的切片最好使用copy函数,注意这里的元素都是值类型。如果是引用类型的话copy的元素还是与原切片关联。

package main

import "fmt"

func main() {
	x := []int{0,1,2}
	x = append(x, 3)
	
	a := make([]int, len(x)+1)
	b := make([]int, len(x)+1)
	
	copy(a, append(x, 4))
	copy(b, append(x, 5))			// 如果不用copy的方式,此时a[4]==5, b[4]==5. 使用的是同一款内存
	
	// output x is [0 1 2 3]
	fmt.Println("x is", x)
    // output a is [0 1 2 3 4]
	fmt.Println("a is", a)
    // output b is [0 1 2 3 5]
	fmt.Println("b is", b)
	
}

9. string的两种遍历

在golang中字符串有两种直接遍历的方式

    1. 如果使用for range遍历,得到的元素值时utf编码的rune类型
    1. 如果使用传统的for i := ...的方式,得到的是byte类型

所以因为这个差异,其上的两种方式遍历的长度也是不一样的,当然在utf和ascii码相同字符遍历时是没差别的。比如纯英文。

待续。。。

miniconda的使用

miniconda的使用

缘由: Anaconda包含了大多数包,特别是人工智能方面,但是占据空间太大。Miniconda比较轻量级,需要什么包安装什么包。本着想用多少就用多少的原则,本人选择使用Miniconda使用。

安装

略。。。

创建虚拟环境

conda create -n python_3.9 python=3.9

conda create -n env_name python=version

切换环境

# 切换到刚刚创建的pythone_3.9环境
conda activate python_3.9

# 删除环境 三思
conda remove -n python_3.9 --all

# 退出
conda deactivate

# 回到默认环境
conda activate base

安装包

conda install 包名

pip可以使用requirements文件来快速安装,当然也可以生成requirements文件。pip使用规范如下:

pip freeze > requirements.txt
pip install -r requirements.txt

conda 可以使用这个文件来安装:

conda install --yes --file requirements.txt

conda也可以完全把环境导出,导出成.yml文件

conda env export > freeze.yml

然后通过导出的文件直接创建conda环境

conda env create -f freeze.yml

WordPress 环境的搭建

WordPress 一套安装流程

1. php7.4 安装

这边使用的是apt

sudo apt install php7.4

然后相应的把php-mysql和php-fpm安装了

sudo apt install php7.4-mysql php7.4-fpm

选择性更改php-fpm的配置文件

sudo vim /etc/php/7.4/fpm/php-fpm.conf
listen = /run/php/php7.4-fpm.sock
;listen = 127.0.0.1:9000

默认就是以上配置,不需要修改,除非你打算使用tcp通讯

修改权限:

 chmod 777 /run/php/php7.4-fpm.sock

nginx 安装

nginx使用的是源码安装,源码地址: https://nginx.org/en/download.html

下载最新版后进入目录,执行

mkdir build
./configure --prefix=./build --with-http_ssl_module
make
make install

这时候build文件夹下的内容就是我们需要的内容, 由于我没安装在全局,所以接下来的内容需要跟着我的步骤来,当然您也可以选择安装在某个path路径下。

|-- build
	|-- conf
	|-- html
	|-- logs
	|-- sbin

将build文件夹移到你要存放的位置,比如/home/xx/nginx/ 下,然后将 sbin 下的那个二进制可执行文件 nginx link到 nginx 文件夹下。

最后修改配置文件conf/nginx.conf

 root   html;
 location / {
 	index  index.html index.htm index.php;
 }

上面的index部分,增加了index.php

 location ~ \.php$ {
            root           html;
            fastcgi_pass   unix:/run/php/php7.4-fpm.sock;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME /home/xx/nginx/build/html$fastcgi_script_name;
            include        fastcgi_params;
        }

上面改变了fastcgi_pass、fastcgi_param。

其中/home/xx/nginx/build/html这串是你的php程序目录名。

WordPress 安装

中文版下载网址: https://cn.wordpress.org/download/

将压缩包解压至/home/xx/nginx/build/html/下, 这是index.php应该会发生替换,选择替换。

Mysql8.x 的安装和初始话

虽然wordpress官网建议是mysql5.6和mariadb10.1或者更高版本,但是由于Mysql8.x已经出来很久了,我这也是用了8.x;

同样apt安装

sudo apt install mysql-server				// 默认安装的就是8.x,

安装完毕之后就是狗血的进入mysql的过程了,折腾了好久,现在是随机密码。

我不知道能不能直接通过以下指令直接进入(需要该linux下的root账户授权的方式进入)

sudo mysql -u root

我是经过以下指令可以进入了(可能在此之前要关闭mysql的service)

sudo mysqld_safe --skip-grant-tables --skip-networking &

之后进入mysql后

FLUSH PRIVILEGES;
use mysql;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyNewPass';

CREATE USER 'guest'@'*' IDENTIFIED BY 'guest123';		// 创建用户

mysql用户部分就告一段落了

flush privileges;
create database test DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

授权部分

grant all privileges on 数据库名.* to 'user1'@'%';
all 可以替换为 select,delete,update,create,drop
flush privileges;

最后的串联

在/home/xx/nginx/文件夹下 有一个链接,指向./build/sbin/nginx, 所以可以执行以下命令

./nginx   

// 这时nginx就在后台运行了
lsof -i:8080 查看
或者
netstat -lnt
或者
ps -aux | grep nginx

之后打开localhost:8080, 可以按wordpress的引导安装了。


告一段落, 当然接下来还会遇到很多问题。但是本文只将如何搭建wordpress的环境,而不讲使用时碰到的一些奇奇怪怪的问题。

ps.
而且centos装php7.x需要额外源
参考: https://linuxize.com/post/install-php-7-on-centos-7/
在服务器上跑时,应该赋予php-fpm运行者权限。否则会出现nofound
chown -R apache:apache /root

arch linux 小问题记录

  1. 在gnome桌面环境下, QT编写的软件可能出现不能支持中文输入, 如Telegram。
    解决方式:
~/.pam_environment
GTK_IM_MODULE DEFAULT=fcitx
QT_IM_MODULE  DEFAULT=fcitx
XMODIFIERS    DEFAULT=\@im=fcitx

后来发现,在kde环境下的那个系统之前安装时就已经修改该文件。

  1. docker-wechat安装
    先说缺点: 这种安装方式比较占存储空间
    docker安装:
sudo pacman -Syu docker
sudo gpasswd -a ${USER} docker

/etc/docker/daemon.json
{
    "registry-mirrors": ["https://registry.docker-cn.com", "https://hub-mirror.c.163.com"]
}

sudo systemctl restart docker

xhost :

huan/docker-wechat#26

xhost +

dochar ------ docker-wechat:

curl -sL https://raw.githubusercontent.com/huan/docker-wechat/master/dochat.sh | bash

以上我无法执行,应该是墙的原因,所以直接吧shell脚本的文本复制出来运行。

脚本复制到本地的好处就是随时运行。

脚本我放在本仓库的linux/下

因为xhost +每次启动后都要执行,所以我这边的脚本里加了这一句了。

树莓派在linux环境下配置成路由器的最简操作

树莓派在linux环境下配置成路由器的最简操作

  1. 前置
  2. 安装软件
  3. 配置网卡
  4. 配置hostapd
  5. 配置dnsmasq
  6. 配置iptables
  7. 开启ipv4转发
  8. 配置开机自启动

0.前置

硬件环境: 树莓派3 b+

软件环境为树莓派官方系统32位, 建议更换软件源。

sudo vim /etc/apt/sources.list
// 注释掉原来的源,添加新源
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
vim /etc/apt/sources.list.d/raspi.list
deb http://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui

1. 安装软件

sudo apt install dnsmasq hostapd

前者为dns和DHCP服务器。后者可以使无线网卡成为无线接入点。

2.配置网卡

sudo vim /etc/network/interfaces
然后将下面这段插入第一条非注释语句的前面
----
auto eth0
allow-hotplug eth0
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet static
    address 192.168.2.1
    netmask 255.255.255.0
    network 192.168.2.0
    broadcast 192.168.2.255

up iptables-restore < /etc/iptables.ipv4.nat

第一块配置的是有限网卡, 设置为自动连接。

第二部分是无线网卡,记住一旦这样配置后,接下来它就无法用于连接wifi了。我这边将其配置成192.168.2.1,忘了不与其他路由器重合。广播地址192.168.2.255,网络地址192.168.2.0,掩码/24,网关192.168.2.1

第三步部分是调用iptables的配置文件。该配置文件应该是会将两个网关进出ip包连成一个通道(第五步骤会做)。

3.配置hostapd

sudo vim /etc/hostapd/hostapd.conf
---
可能会没有这个文件,反正会新建
----
# This is the name of the WiFi interface we configured above
interface=wlan0

# Use the nl80211 driver with the brcmfmac driver
driver=nl80211

# This is the name of the network
ssid=passwd-is-81

# Use the 2.4GHz band
hw_mode=g

# Use channel 6
channel=6

# Enable 802.11n
ieee80211n=1

# Enable WMM
wmm_enabled=1

# Enable 40MHz channels with 20ns guard interval
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]

# Accept all MAC addresses
macaddr_acl=0

# Use WPA authentication
auth_algs=1

# Require clients to know the network name
ignore_broadcast_ssid=0

# Use WPA2
wpa=2

# Use a pre-shared key
wpa_key_mgmt=WPA-PSK

# The network passphrase
wpa_passphrase=11111111

# Use AES, instead of TKIP
rsn_pairwise=CCMP

热点名passwd-is-81, 密码11111111.其他方式和一般2.4ghz频率的信号一样.

4. 配置dnsmasq

sudo vim /etc/dnsmasq.conf
---
如果该文件存在东西了,可以注释掉
----

# Use interface wlan0
interface=wlan0

# Explicitly specify the address to listen on
listen-address=192.168.2.1

# Bind to the interface to make sure we aren't sending things elsewhere
bind-interfaces

# Forward DNS requests to Google DNS
server=114.114.114.114

# Don't forward short names
domain-needed

# Never forward addresses in the non-routed address spaces
bogus-priv

# Assign IP addresses between 192.168.2.2 and 192.168.2.254 with a 12 hour lease time
dhcp-range=192.168.2.2,192.168.2.254,12h

记住这边的IP地址,除了dns-server的,其他都要和网卡信息符合,也就是你之前配置的。

5. 配置iptables

介绍: 可以把iptables理解成是一个防火墙,其实本身就是防火墙。在做代理时也会用到它。

sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE  
sudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT  
sudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT 

以下部分是记录下当前iptables, 每次重启恢复一遍(恢复操作是在2.配置网卡最后一块配置里执行)

sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"

6.开启ipv4转发

sudo vim /etc/sysctl.conf
----
将net.ipv4.ip_forward=1反注释掉,或者直接在最后输入
----
net.ipv4.ip_forward=1

或者直接以下操作

sudo echo 1 > /proc/sys/net/ipv4/ip_forward

7.配置开机自启动

使用 systemctl 工具配置 dnsmasq 和 hostapd 开机自启动。将其中的 enable 替换为 start 和 status ,可以实现立即启动软件和查看软件当前状态。

$ sudo systemctl enable dnsmasq
$ sudo systemctl enable hostapd

我在配置时出现错误信息 Failed to start hostapd.service: Unit hostapd.service is masked. 使用下面命令可以解决。

$ sudo systemctl unmask hostapd

golang交叉编译

关于在linux和mac下交叉编译其他平台

CGO_ENABLE=0 GOOS=linux GOARCH=amd64 go build main.go
CGO_ENABLE=0 GOOS=darwin GOARCH=amd64 go build main.go
CGO_ENABLE=0 GOOS=windows GOARCH=amd64 go build main.go

关于windows上交叉编译其他平台

cmd下

SET CGO_ENABLE=0 
SET GOOS=linux 
SET GOARCH=amd64 
go build main.go

全部

可以go1.13后可以使用工具链

go env -w GOOS=linux GOARCH=amd64 CGO_ENABLED=0

总结

GOOS: darwin freebsd linux windows
GOARCH: 386 amd64 arm
交叉编译不支持CGO(windows)
其实就先设置临时环境变量在编译

electron+webpack+react使用two-package结构搭建桌面应用项目

electron+webpack+react项目

renderer进程使用的是 https://github.com/GuoYuefei/simple_react_webpack 的第四次commit的模板

react+react搭建项目的模板

https://github.com/GuoYuefei/simple_react_webpack/blob/master/README.md


electron部分

two package目录结构

目录结构使用的是electron-build推荐的two package结构。但是结构本身加入了些我个人的理解。

两个package.json的分别是package.json 和 /app/package.json. 可以理解为前者是给webpack使用的,后者是给electron看的。我说的webpack包括了它所有利用到的工具包括babel在内。还有本人用的yaml来代替package.json来配置electron-builder,所以理论上讲本段第一句行得通。

事实我找官方给出的two package目录了,貌似electron-build没有明确给出。本人也是在一次项目经历中才发现electron会把package.json的dependencies打包起来,导致安装包特别大。所以想到方法是用webpack打包main,然后使用一个空的dependencies的package.json。此时突然想起之前看到的two-package目录结构,一下子理解了为什么要这么做。。。。其实就是把electron项目和js项目分开,其实就是两个项目,只是放在一起罢了。

+ app									// 作为webpack的输出目录, 和electron的源目录
  + assets					    		// 放一些资源文件
  + dist								// 这是webpack打包的输出目录
    + main								// 这是webpack对主进程程序打包的构建目录
    + renderer							// 这是webpack对渲染进程程序打包的构建目录
  + node_modules						// electron执行后产生,仅一个文件,其余为空
  + pages								// 页面放在这
    - index.html						// 这是渲染进程的主页面
  - package.json						// 给electron看的,app的名字和版本等在此配置
+ config								// 放开发配置文件的地方
  - electron-builder.yml				// electron-builder的配置文件
  - webpack.config.main.prod.js			// webpack的主进程打包配置文件
  - webpack.config.renderer.prod.js		// webpack的渲染进程打包配置文件
  ...									// 其余的配置文件,如开发环境下的配置文件
+ dist						// 最终的构建目录,也就是electron-builder设置的构建目录
  + mac									// 构建mac程序生成目录
  + win-unpacked						// 构建win x64程序时生成目录
  ....									// 其余文件,如dmg和windows安装包文件
+ node_modules							// 真正放依赖的地方
+ public					// 放一些公共内容, 应该可以由app/assets 和 app/pages代替
+ src									// 源代码
  + main								// main源代码
  + renderer							// 渲染部分代码
  + shared								// 公共部分代码
- package.json							// 记录所有依赖

以上即是整个项目目录及作用。其中electron-builder会在当项目下有app目录时自动以该目录为根目录。

模板项目

https://github.com/GuoYuefei/simple_electron_react_webpack
https://www.notion.so/gyf/electron-webpack-react-two-package-3312c13eb05e44779501baea4fe81403

arch linux mysql

数据库安装mariadb
因为很多linux发行版都放弃了对mysql的支持(原因自行百度)转而支持mariadb(mysql的另一个分支),Archlinux就是其中之一,mariadb具有和mysql一模一样的操作命令,所以完全不用考虑迁移兼容的问题
注意ArchLinux安装mysql是不可行的,不要试图安装mysql,那是不成功的
1.安装mariadb
sudo pacman -Sy mariadb
2.配置mariadb命令,创建数据库都在/var/lib/mysql/目录下面
sudo mysql_install_db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
3.开启mariadb 服务
systemctl start mariadb
4.初始化密码 期间有让你设置密码的选项 设置你自己的密码就行了 然后一路y就行
sudo /usr/bin/mysql_secure_installation
5.登录mariadb 和mysql命令是一样的
mysql -u root -p

defer的一些坑

defer的一些坑

核心点

核心1: defer是在return之前执行的。

核心2: return本身不是一条原子操作。

坑的例子

package main

import "fmt"

func main() {
	fmt.Println(keng1())
	fmt.Println(keng2())
	fmt.Println(keng3())
	fmt.Println(keng4())
}

/**
	return 0, 不是原子操作,先赋值后返回
	其中赋值与返回之间执行defer内容, 这就是官方资料中说的所谓的在return之前defer
 */
// output 1
func keng1() (ret int) {
	defer func() {
		ret++
	}()
	return 0
}

// output 5
func keng2() (ret int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}

// output 1
func keng3() (r int) {
	defer func(r int) {
		r += 5
	}(r)
	return 1
}

/**
	不带命名参数
	keng4的情况其实是和keng2一样的, keng4无返回命名,但是返回值的地址空间还是有的
	执行步骤如下
	ret := 0
	result = ret
	defer func() { ret++ }()
	return result
 */
// output 0
func keng4() int {
	ret := 0
	defer func() {
		ret++
	}()
	return ret
}

这边有四个坑,无论是哪种坑,只要把defer和return拿出来,做上面核心所说的分析就行了。

如keng1函数

// output 1
func keng1() (ret int) {
	defer func() {
		ret++
	}()
	return 0
}

其实可以翻译成

ret = 0
ret++
return			// 这部分return的是ret, so output is 1

如keng2函数

// output 5
func keng2() (ret int) {
	t := 5
	defer func() {
		t = t + 5
	}()
	return t
}

其实可以翻译成

t := 5
// 以下开始是return拆分,并参入了defer的内容
ret = t
t = t + 5
return 			// 这部分return的是ret, so output is 5

keng3函数自行分析,直接跳到keng4函数,为什么说它和keng2是一样的呢?

// output 0
func keng4() int {
	ret := 0
	defer func() {
		ret++
	}()
	return ret
}

没有用命名的返回值,但是我们可以认为存在(事实上地址空间还是开辟着的),所以暂且用result表示这块返回区域

ret := 0
// you know
result = ret
ret++
return 				// return 的是result, so output is 0

从翻译内容来看,就是keng2的内容。

接口类型值和接口值问题,包括nil值问题

接口类型值和接口值问题,包括nil值问题

引言

先来看一个错误的程序

const debug = true
func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // enable collection of output
    }
    f(buf) // NOTE: subtly incorrect!
    if debug {
        // ...use buf...
    }
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
    // ...do something...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

debug=true ,开启状态是没有问题的。

但当 debug=false ,运行时就会出现空指针问题,out.Write([]byte("done!\n"))

原因在于out接口其动态类型为 *bytes.Buffer, 但是其接口值为nil。 ---> 原因是: 实参未被赋值,而是这个类型的空值

而nil的类型的动态类型和值都是nil,所以,nil != out 是为 true 的。


iface and eface

可以从源码层面看下

普通接口配型有iface类型定义, 空接口为eface

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}

以上为iface的定义, iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。

再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。

这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。如果你学过 C++ 的话,这里可以类比虚函数的概念。

另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。

再看一下 interfacetype 类型,它描述的是接口的类型:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。

接着来看一下 eface 的源码:

type eface struct {
  _type *_type
  data  unsafe.Pointer
}

相比 ifaceeface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。

接口的动态类型和动态值

从源码里可以看到:iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型动态值。而接口值包括动态类型动态值

【引申1】接口类型和 nil 作比较 (引言中的程序错误的原因)

接口值的零值是指动态类型动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil

例子:

package main
import "fmt"
type Coder interface {
    code()
}
type Gopher struct {
    name string
}
func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}
func main() {
    var c Coder
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
    var g *Gopher
    fmt.Println(g == nil)
    c = g
    fmt.Println(c == nil)
    fmt.Printf("c: %T, %v\n", c, c)
}

输出为:

true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>

【引申2】来看一个例子,看一下它的输出:

package main
import "fmt"
type MyError struct {}
func (i MyError) Error() string {
    return "MyError"
}
func main() {
    err := Process()
    fmt.Println(err)
    fmt.Println(err == nil)
}
func Process() error {
    var err *MyError = nil
    return err
}

函数运行结果:

<nil>
false

这里先定义了一个 MyError 结构体,实现了 Error 函数,也就实现了 error 接口。Process 函数返回了一个 error 接口,这块隐含了类型转换。所以,虽然它的值是 nil,其实它的类型是 *MyError,最后和 nil 比较的时候,结果为 false所以返回错误要返回无错误时,应该直接返回nil,而非用先声明再返回的方式

【引申3】如何打印出接口的动态类型和值?

直接看代码:

package main
import (
    "unsafe"
    "fmt"
)
type iface struct {
    itab, data uintptr
}
func main() {
    var a interface{} = nil
    var b interface{} = (*int)(nil)
    x := 5
    var c interface{} = (*int)(&x)
    ia := *(*iface)(unsafe.Pointer(&a))
    ib := *(*iface)(unsafe.Pointer(&b))
    ic := *(*iface)(unsafe.Pointer(&c))
    fmt.Println(ia, ib, ic)
    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

代码里直接定义了一个 iface 结构体,用两个指针来描述 itabdata,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface。最后就可以打印出动态类型和动态值的地址。

Output:

{0 0} {17426912 0} {17426912 842350714568}
5

a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int;最后,c 的动态值为 5。

以上自定义inface强转的方法, 在以下参考资料的《Go 语言问题集》的‘标准库’的‘unsafe’部分有讲。是本好书,值得反复看。

参考

《Go 语言问题集》

《Go语言圣经中文版(简体)》

指令优化和内存重排对并发程序的影响

指令优化和内存重排对并发程序的影响

引出问题

先来上码,代码比较神奇,第一眼见他就深深的吸引了我。

package main

import (
  	"fmt"
  	"time"
  	"runtime"
)

func main() {
    var x int
    threads := runtime.GOMAXPROCS(0)
    for i := 0; i < threads; i++ {
        go func() {
            for { x++ }
        }()
    }
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

据作者说,这段代码不会运行结束,而是陷入死循环。当然该作者运行这段代码的时候还是go1.9.x版本,后期go已经得到这方面的优化了。

会陷入死循环的原因,作者的解释是:

上面的例子会启动和机器的 CPU 核心数相等的 goroutine,每个 goroutine 都会执行一个无限循环。

创建完这些 goroutines 后,main 函数里执行一条 time.Sleep(time.Second) 语句。Go scheduler 看到这条语句后,简直高兴坏了,要来活了。这是调度的好时机啊,于是主 goroutine 被调度走。先前创建的 threads 个 goroutines,刚好“一个萝卜一个坑”,把 M 和 P 都占满了。

在这些 goroutine 内部,又没有调用一些诸如 channeltime.sleep 这些会引发调度器工作的事情。麻烦了,只能任由这些无限循环执行下去了。

但事实上并不会出现这种情况,原因是go程序还存在g0协程和sysmon后台监控线程,不至于出现以上的问题。本人没在go1.9.x环境运行过这段代码,有兴趣的朋友可以试下。

为什么上这份代码?

原因是,这个会输出x = 0的情况。为啥1s过去了,x还是0?难以置信。

原因解答

其发生的主要原因是存在各级缓存和store buffer。

+---------------+             +--------------+
|	  Core1		|             |	   Core2     |
|			    |             |				 |
|_______________|             |______________|	
| x |   |   |   |	          |  |   |   |   |
+---------------+             +--------------+
|   L1 Cache	|             |	  L1 Cache	 |
+---------------+             +--------------+
|   L2 Cache	|             |	  L2 Cache	 |
+---------------+-------------+--------------+
|                   L3 Cache                 |
+--------------------------------------------+
                      |
==================== bus ==================== >
                      |
+---------------------------------------------+
|                                             |
|                    Memory                   |
|                                             |
+---------------------------------------------+

x 会在从Memory读取到store buffer,然后core1在一直写x以至于x一直没有能够刷新到L3 Cache或者内存。此时,主协程所在的核心对core1修改x这一行为是无感知的,他从内存中取出的x一直为0.

问题修复

package main

import (
	"fmt"
  "time"
  "runtime"
)

func main() {
  var x int
  var done chan bool = make(chan bool)
  threads := runtime.GOMAXPROCS(0)
  for i := 0; i < threads; i++ {
    go func() {
      for {
        select {
        case <- done: return
        default: x++
        }
      }
    }()
  }
  time.Sleep(time.Second)
  close(done)
  fmt.Println("x =", x)
}

这是所有的x++协程就会在主协程发出关闭命令后关闭,刷新到内存。其实理解这样没毛病,但是go貌似在含通道操作的协程上做了些好事,做了些同步操作。

这边启动了和核心相同的协程,当将协程数量改至1时,x的值会比多协程的情况下大!?本人电脑尝试出来是相差一个数量级。

猜想原因是单协程x++时,可以不用考虑写同步,只要在store buffer下操作x,直到主协程访问x之前刷新进内存或L3 Cache。而多协程x++时会需要考虑同步问题,导致效率低下。

效率相差比较大,以后编程时可以注意这方面,特别是有频繁的对一个变量读写时。

还有引起其他有趣的问题,有些还难以解释,有机会继续探索。。

unsafe 包的使用

unsafe 包的使用

go指针的限制

  1. go的指针不能进行数学运算
  2. 不同类型的指针不能相互转换
  3. 不同类型的指针不能使用 == 或 != 比较
  4. 不同类型的指针变量不能相互赋值

unsafe 包提供了 2 点重要的能力

  1. 任何类型的指针和 unsafe.Pointer 可以相互转换。
  2. uintptr 类型和 unsafe.Pointer 可以相互转换。

ps. uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

unsafe 包可以做什么坏事

1.绕过私有成员的限制

可以通过unsafe包绕过私有属性限制对私有属性读写。

package A

type A struct {
	name string
	age int
    mark bool
}
package main

import (
    "A"
	"fmt"
    "unsafe"
)

func main() {
    a := A.A{}
    fmt.Println(a)
    
    name := (*string)(unsafe.Pointer(&p))
    age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string(""))))
    mark := (*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string("")) + unsafe.Sizeof(0)))
    
    *name = "A"
    *age = 20
    *mark = false
    
    // output {A 20 false}
    fmt.Println(a)
}

2.通过伪造快速对私有属性更改

这边以内置类型slice为例,可以参考slice的源码。

ps. 与本主题无关的提示: make得到的slice是实体,make得到的map是指针。

package main

import (
	"fmt"
	"unsafe"
)

// 也可以使用小技巧,构造一个和 slice 一样的结构体来解析或操作切片
type slice struct {
	arrptr unsafe.Pointer
	l int
	c int
}

func main() {
	a := []int{1,2}
	a = append(a, 3)
	length := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + unsafe.Sizeof(unsafe.Pointer(&a))))

	ca := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + unsafe.Sizeof(unsafe.Pointer(&a)) + unsafe.Sizeof(int(0))))

	fmt.Println(*length, *ca)

	*length = *length + 1				// + 1024 没问题, 只要在保护的内存段中就行
	//a = append(a, 4)
	fmt.Println(a, *length, *ca)

	s := *(*slice)(unsafe.Pointer(&a))

	fmt.Println(s)
	fmt.Printf("[%d, %d, %d, %d]\n", *(*int)(s.arrptr), *(*int)(unsafe.Pointer(uintptr(s.arrptr) + unsafe.Sizeof(int(0)))),
		*(*int)(unsafe.Pointer(uintptr(s.arrptr) + 2*unsafe.Sizeof(int(0)))),
		*(*int)(unsafe.Pointer(uintptr(s.arrptr) + 3*unsafe.Sizeof(int(0)))),
		)

}

关于protobuf的安装及自定插件

关于protobuf的安装及自定插件

protobuf 二进制文件安装

直接下以下网址下载

https://github.com/protocolbuffers/protobuf

然后将二进制和include放在path中

插件 protoc-gen-go 下载

https://github.com/protocolbuffers/protobuf-go

仓库地址虽然是在github,但是使用 go get 的时候不能使用这个仓库地址,原因是 mod 文件设置的仓库和go get的地址不一样

所以查看 mod 文件,之后找到目标地址。
这个star量比较少,这里的仓库维持这最新版本。

// 2020.08.03
go get google.golang.org/protobuf/cmd/protoc-gen-go
protoc --go_out=. xxx.proto

插件 protoc-gen-go-grpc 安装

https://github.com/grpc/grpc-go
和上面相同的情况,必须使用以下地址go get

// 2020.08.03
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
protoc --go-grpc_out=. xxx.proto

插件使用命令

例:

protoc --go-netrpc_out=plugins=netrpc:. ./protoc/hello.proto

基于 protoc-gen-go 的自定义最小插件

package main

import (
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/protoc-gen-go/descriptor"
	"github.com/golang/protobuf/protoc-gen-go/generator"
	"io/ioutil"
	"os"
)
// 自定义
type netrpcPlugin struct{ *generator.Generator }
func (p *netrpcPlugin) Name() string                { return "netrpc" }
func (p *netrpcPlugin) Init(g *generator.Generator) { p.Generator = g }
func (p *netrpcPlugin) GenerateImports(file *generator.FileDescriptor) {
	if len(file.Service) > 0 {
		p.genImportCode(file)
	}
}
func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
	for _, svc := range file.Service {
		p.genServiceCode(svc)
	}
}


func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
	p.P("// TODO: import code")
}
func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
	p.P("// TODO: service code, Name = " + svc.GetName())
}

// 注册 plugin
func init() {
	generator.RegisterPlugin(new(netrpcPlugin))
}

// main 来自protoc-gen-go 的源码
func main() {
	g := generator.New()
	data, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		g.Error(err, "reading input")
	}
	if err := proto.Unmarshal(data, g.Request); err != nil {
		g.Error(err, "parsing input proto")
	}
	if len(g.Request.FileToGenerate) == 0 {
		g.Fail("no files to generate")
	}
	g.CommandLineParameters(g.Request.GetParameter())
	// Create a wrapped version of the Descriptors and EnumDescriptors that
	// point to the file that defines them.
	g.WrapTypes()
	g.SetPackageNames()
	g.BuildTypeNameMap()
	g.GenerateAllFiles()
	// Send back the results.
	data, err = proto.Marshal(g.Response)
	if err != nil {
		g.Error(err, "failed to marshal output proto")
	}
	_, err = os.Stdout.Write(data)
	if err != nil {
		g.Error(err, "failed to write output proto")
	}
}

一个普通rpc含服务的插件

来源: go语言高级编程

package main

import (
	"bytes"
	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/protoc-gen-go/descriptor"
	"github.com/golang/protobuf/protoc-gen-go/generator"
	"io/ioutil"
	"log"
	"os"
	"text/template"
)
type netrpcPlugin struct{ *generator.Generator }
func (p *netrpcPlugin) Name() string                { return "netrpc" }
func (p *netrpcPlugin) Init(g *generator.Generator) { p.Generator = g }
func (p *netrpcPlugin) GenerateImports(file *generator.FileDescriptor) {
	if len(file.Service) > 0 {
		p.genImportCode(file)
	}
}
func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
	for _, svc := range file.Service {
		p.genServiceCode(svc)
	}
}

func (p *netrpcPlugin) genImportCode(file *generator.FileDescriptor) {
	p.P(`import "net/rpc"`)
}

func (p *netrpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
	spec := p.buildServiceSpec(svc)
	var buf bytes.Buffer
	t := template.Must(template.New("").Parse(tmplService))
	err := t.Execute(&buf, spec)
	if err != nil {
		log.Fatal(err)
	}
	p.P(buf.String())
}

// 服务分析
// 服务名称和方法列表
type ServiceSpec struct {
	ServiceName string
	MethodList  []ServiceMethodSpec
}

// 方法列表需要方法名称, 输入输出的类型
type ServiceMethodSpec struct {
	MethodName     string
	InputTypeName  string
	OutputTypeName string
}

// 生成服务的描述
func (p *netrpcPlugin) buildServiceSpec(
	svc *descriptor.ServiceDescriptorProto,
) *ServiceSpec {
	spec := &ServiceSpec{
		ServiceName: generator.CamelCase(svc.GetName()),
	}
	for _, m := range svc.Method {
		spec.MethodList = append(spec.MethodList, ServiceMethodSpec{
			MethodName:     generator.CamelCase(m.GetName()),
			InputTypeName:  p.TypeName(p.ObjectNamed(m.GetInputType())),
			OutputTypeName: p.TypeName(p.ObjectNamed(m.GetOutputType())),
		})
	}
	return spec
}

const tmplService = `
{{$root := .}}
type {{.ServiceName}}Interface interface {
    {{- range $_, $m := .MethodList}}
    {{$m.MethodName}}(*{{$m.InputTypeName}}, *{{$m.OutputTypeName}}) error
    {{- end}}
}
func Register{{.ServiceName}}(
    srv *rpc.Server, x {{.ServiceName}}Interface,
) error {
    if err := srv.RegisterName("{{.ServiceName}}", x); err != nil {
        return err
    }
    return nil
}
type {{.ServiceName}}Client struct {
    *rpc.Client
}
var _ {{.ServiceName}}Interface = (*{{.ServiceName}}Client)(nil)
func Dial{{.ServiceName}}(network, address string) (
    *{{.ServiceName}}Client, error,
) {
    c, err := rpc.Dial(network, address)
    if err != nil {
        return nil, err
    }
    return &{{.ServiceName}}Client{Client: c}, nil
}
{{range $_, $m := .MethodList}}
func (p *{{$root.ServiceName}}Client) {{$m.MethodName}}(
    in *{{$m.InputTypeName}}, out *{{$m.OutputTypeName}},
) error {
    return p.Client.Call("{{$root.ServiceName}}.{{$m.MethodName}}", in, out)
}
{{end}}
`


func init() {
	generator.RegisterPlugin(new(netrpcPlugin))
}

func main() {
	g := generator.New()
	data, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		g.Error(err, "reading input")
	}
	if err := proto.Unmarshal(data, g.Request); err != nil {
		g.Error(err, "parsing input proto")
	}
	if len(g.Request.FileToGenerate) == 0 {
		g.Fail("no files to generate")
	}
	g.CommandLineParameters(g.Request.GetParameter())
	// Create a wrapped version of the Descriptors and EnumDescriptors that
	// point to the file that defines them.
	g.WrapTypes()
	g.SetPackageNames()
	g.BuildTypeNameMap()
	g.GenerateAllFiles()
	// Send back the results.
	data, err = proto.Marshal(g.Response)
	if err != nil {
		g.Error(err, "failed to marshal output proto")
	}
	_, err = os.Stdout.Write(data)
	if err != nil {
		g.Error(err, "failed to write output proto")
	}
}

关于math.NaN() 的比较问题

关于math.NaN() 的比较问题

缘起

缘起于二刷map底层时看到的一个例子

func main() {
    m := make(map[float64]int)
    m[1.4] = 1
    m[2.4] = 2
    m[math.NaN()] = 3
    m[math.NaN()] = 3
    for k, v := range m {
        fmt.Printf("[%v, %d] ", k, v)
    }
    fmt.Printf("\nk: %v, v: %d\n", math.NaN(), m[math.NaN()])
    fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001])
    fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])
    fmt.Println(math.NaN() == math.NaN())
}

Output :

[2.4, 2] [NaN, 3] [NaN, 3] [1.4, 1] 
k: NaN, v: 0
k: 2.400000000001, v: 0
k: 2.4, v: 2
false

由此证明, NaN 是不等于 NaN 的,然后我就在那边纠结这个为啥不相等呢? 我们都知道接口有动态类型和动态值相同的限制才算相等,但是关键注意 NaN 本质上将还是 float64 类型, 也就是说关于接口的比较法则是不适用于它。

于是我在那边查了半天资料,一无所获。(可能是我查资料的姿势不对吧)于是乎,决定自己动动手,找找看。

直接搜寻 Go 的 源码(无结论)

// go version is 1.14.6
// runtime/alg.go 
func f64equal(p, q unsafe.Pointer) bool {
	return *(*float64)(p) == *(*float64)(q)
}

还是要解读下的,可能刚入门的gopher认为这个不就是直接比较嘛,其实这个函数只是为了统一接口,在这个文件下有很多和这个函数相同函数原型的函数,方便各变量调用相等操作。

可能我找的不够全面,反正从这边我无法找到关于 NaN 的特殊处理方法。

额,既然到了源码这部分,还是来认识下什么是 NaN 吧!

// go version is 1.14.6
// math/bits.go

// NaN returns an IEEE 754 ``not-a-number'' value.
func NaN() float64 { return Float64frombits(uvnan) }

Float64frombits 函数顾名思义就是从比特位层面看int64是代表什么样的float64

关键uvnan是什么, 其实就是一个常数

// go version is 1.14.6
// math/bits.go

const uvnan    = 0x7FF8000000000001

其实看源码的时候你能看到很多常数,有一个很有趣的常数 uvinf = 0x7FF0000000000000,这个当符号位为正时,代表双精度的最大值,为负数时为最小值

根据 IEEE 754 标准呢,NaN 应该是指数部分全1, 小数部分非零, 但是只要小数部分有一个非0,就能区分出是 uvinf 还是 uvnan 了。go语言这边使用的是小数部分最低位为1其余小数部分为0的方式。

现在我们知道 NaN 到底是样什么东西了。

通过汇编找问题所在

// main.go
package main

import "fmt"
//import "unsafe"
import "math"

func main() {
  a := math.NaN()

  b := xxxx(a)

  fmt.Println(b)
}

func xxxx(a float64) bool {
  r := a == a
  return r
}

以上是我的测试代码, 其实可以不用fmt, 通过-N方式关闭编译优化就行。 使用一个xxxx函数的原因是为了方便定位比较这块的汇编代码。

使用以下命令打印出汇编指令

go tool compile -S main.go

然后找到这块内容,这是我们需要的

"".xxxx STEXT nosplit size=23 args=0x10 locals=0x0
	0x0000 00000 (const.go:15)	TEXT	"".xxxx(SB), NOSPLIT|ABIInternal, $0-16
	0x0000 00000 (const.go:15)	PCDATA	$0, $-2
	0x0000 00000 (const.go:15)	PCDATA	$1, $-2
	0x0000 00000 (const.go:15)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (const.go:15)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (const.go:15)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0000 00000 (const.go:16)	PCDATA	$0, $0
	0x0000 00000 (const.go:16)	PCDATA	$1, $0
	0x0000 00000 (const.go:16)	MOVSD	"".a+8(SP), X0
	0x0006 00006 (const.go:16)	UCOMISD	X0, X0
	0x000a 00010 (const.go:16)	SETEQ	CL
	0x000d 00013 (const.go:16)	SETPC	AL
	0x0010 00016 (const.go:16)	ANDL	AX, CX
	0x0012 00018 (const.go:17)	MOVB	CL, "".~r1+16(SP)
	0x0016 00022 (const.go:17)	RET
	0x0000 f2 0f 10 44 24 08 66 0f 2e c0 0f 94 c1 0f 9b c0  ...D$.f.........
	0x0010 21 c1 88 4c 24 10 c3                             !..L$..

MOVSD "".a+8(SP), X0 这部分开始一直到 RET,都是我们所需要的。

MOVSD	"".a+8(SP), X0 					; 将函数的第一个参数放入X0中 		双字长
UCOMISD	X0, X0							; 无序比较					就是这一步,等会慢慢讲
SETEQ	CL										; CL寄存器中存入ZF标志位
SETPC	AL										; AL寄存器中存入PF标志位	奇偶标志位		// 参照SETNP指令 后来发现其实不是取PF,而是取PF反后放入AL寄存器
ANDL	AX, CX									; 按位与运算
MOVB	CL, "".~r1+16(SP)				; 与运算结果放入返回值的地址上
RET														; 返回

比较的关键在UCOMISD指令的作用上。

无序比较操作符 uncomisd 的作用如下:

(V)UCOMISD (all versions)

RESULT← UnorderedCompare(DEST[63:0] <> SRC[63:0]) {
(* Set EFLAGS *) CASE (RESULT) OF
    UNORDERED: ZF,PF,CF←111;
    GREATER_THAN: ZF,PF,CF←000;
    LESS_THAN: ZF,PF,CF←001;
    EQUAL: ZF,PF,CF←100;
ESAC;
OF, AF, SF←0; }

由此可见,当为无序时,也就是出现一个比较操作符是NaN时, ZF, PF, CF都为1, ZF AND PF == 1。 其他情况,ZF AND PF == 0, 这就做出了区分. 将按位与结果CL放入放回值地址,return函数。ps. 为嘛用0代表true, 感觉不对啊。 而且普通数比较相等还是不相等也没区分啊???所以上面一定有遗漏。

package main

import "fmt"
import "unsafe"

func main() {
  a := true
  c := *(*byte)(unsafe.Pointer(&a))
  fmt.Printf("%d", c)
}

Ouput :

1

事实证明true是在内存里为1的。

最后我把错误锁定在了对 setpc 指令的理解上。

SETPC ,也就是普通汇编中SETNP这个指令,是当PF寄存器为0时,取值才为1.

所以最后CL寄存器中的值应该为ZF AND NOT PF, 所以在无序比较中一旦有NaN存在,CL的值1 and 0就为0, 正常比较是不相等0 and 1c也是0, 只有相等时1 and 1才为1.

结语

NaN 是由 IEEE 754 标准定义的, 其比较是由底层汇编 UCOMISD 完成了。 该指令考虑了无序(存在NaN的情况)时的比较。

参考文献:

https://www.felixcloutier.com/x86/ucomisd

https://quasilyte.dev/blog/post/go-asm-complementary-reference/ (GO 汇编和其他汇编的对照表)

https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf (查找SETNP指令)

简单重写 react-redux —> redux-connect

notion地址: https://www.notion.so/gyf/react-redux-redux-connect-7c31117badf645c0a8b9ffb84ea60a31

以下为notion导出markdown之后并修改后的文档


简单重写 react-redux —> redux-connect

状态管理Redux

现在主要流行的状态管理库redux和mobx。

redux大概的原理是用一个store来存储说有的状态,用action去定义修改类型,reducer定义如何修改(根据action),然后用dispatch去触发修改,subscribe去注册监听器,getState获取状态。

诶,本文重点不是redux和react,而是react-redux这个库。所以说吼,就只简单描述下redux了。记住redux很简洁,不与其他库有任何的关联之类的。在node中直接使用redux也是可以的,或者游览器环境下。

React与Redux的连接桥梁

react-redux是react的连接桥梁,将redux作用到了react上。

api见链接:

API

api就两样东西,一个Provider组件,一个connect函数。

详细就看这份中文文档了,不赘(zhui4)述了。

重写react-redux的过程

在看到这份api后,我的第一想法是简单的实现一遍。

Provider可以让人想起react的context, connect可以让人想起高级组件的用法。

所以react-redux本身应该是用context,将redux生成的store传递到Provider,然后在connect中的包裹组件里使用this.context使用它。 context使用:

Context - React

当然原生的Provider接收的属性名是value,所以需要和react-redux的api相同的话还要在前面套一层。

    // in ./redux-connect/Provider.js
    import React from 'react';
    
    const Store = React.createContext();
    
    Store.displayName = "redux-connect";
    
    class Provider extends React.Component {
        render() {
            let props = this.props;
            return (
                <Store.Provider value={props.store || props.value} >
                    {
                        React.Children.map(props.children, (child, i) => {
                            return child
                        })
                    }
                </Store.Provider>
            )
        }
    }
    
    export default Provider;
    export { Store, Provider };

connect的实现重点在对map..ToProps的实现上,方法较上面的复杂些。还有这边的shouldComponentUpdate里的优化请忽略,我是用了深比较,react-redux中在connect一个PureComponent时使用的是浅比较,所以比较的效率会偏好些!但是也要求最好不要出现复杂嵌套的数据,因为浅比较比较不了。这边深比较会出现效率问题,比较序列化耗时。但是与渲染的时间比起来应该好很多。

connect函数少实现了第四个参数:(很少使用并未实现,其实第三个参数也很少使用)

  • connect第四个参数的api

    [options(Object) 如果指定这个参数,可以定制 connector 的行为。

    • [pure = true(Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true
    • [withRef = false(Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false
    // in ./redux-connect/connect.js
    
    // 实现一个没有怎么优化过的并且简单的高阶函数connect
    import React from 'react';
    import { Store } from './Provider'


    export default function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
   
    if (typeof mergeProps !== "function") {
        mergeProps = Object.assign;
    }
    
    return (Component) => {
        class ConnectComponent extends React.Component {
            static contextType = Store;
    
            constructor(props, context) {
                super(props, context);
                this.store = this.props.store || this.context;
                this.stateToProps = this.doMapStateToProps();
                this.dispatchToProps = this.doDispatchToProps();
                this.state = { store: this.doMergeProps() }
            }
            
            componentDidMount() {
    							// 订阅变化
                this.unsub = this.store.subscribe(() => {
                    this.updateState();
                })
            }
    
            shouldComponentUpdate(nextProps, nextState) {
                // console.log("now props: ", this.props, "next props: ", nextProps);
                // console.log("now state: ", this.state, "next state: ", nextState);
                if (JSON.stringify(nextProps) !== JSON.stringify(this.props) || JSON.stringify(nextState) !== JSON.stringify(this.state)) {
                    // console.log("可以刷新")
                    return true;
                }
                // console.log("不能刷新")
                return false;
            }
    
            componentWillUnmount() {
                this.unsub();
            }
    
            doMapStateToProps = () => {
                if(typeof mapStateToProps !== "function") {
                    return {}
                }
                return mapStateToProps(this.store.getState(), this.props || {});
            }
    
            doDispatchToProps = () => {
                if (typeof mapDispatchToProps !== "function") {
                    if (typeof mapDispatchToProps !== "object") {
                        return {};
                    }
                    let dispatchProps = {};
                    console.log(mapDispatchToProps)
                    for(var key in mapDispatchToProps) {
                        // eslint-disable-next-line
                        dispatchProps[key] = (...params) => this.store.dispatch(mapDispatchToProps[key](...params))
                    }
                    return dispatchProps;
                }
                return mapDispatchToProps(this.store.dispatch, this.props || {});
            }
    
            doMergeProps = () => {
                return mergeProps(this.doMapStateToProps(), this.doDispatchToProps(), this.props || {}, { dispatch: this.store.dispatch })
            }
    
            updateState = () => {
                this.setState({
                    ...{store: this.doMergeProps()}
                })
            }
    
            render() {
                return (
                    <Component {...this.state.store}/>
                )
            }
        }
        
        return ConnectComponent;
    }
}

在实际应用中去掉console.log的注释就可以看到shouldComponentUpdate的作用了。

最后一步放入index.js导出

// in ./redux-connect/index.js

import connect from './connect';
import { Store, Provider } from './Provider';

export { connect, Store, Provider };
export default { connect, Store, Provider };

其实这边的Store主要是给connect.js用的,其实完全可以不用导出的。

最后

暂叫redux-connect吧。😂所以放在redux-connect文件夹下,import相对路径使用它吧。

gist地址:

https://gist.github.com/GuoYuefei/d4b706732f3fd55e60e37fbecb4e807e

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.