guoyuefei / blog Goto Github PK
View Code? Open in Web Editor NEW🍪 blog在issues, 存储文件在项目中
License: MIT License
🍪 blog在issues, 存储文件在项目中
License: MIT License
作为少数包含指针的语言,它与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
}
Go语言提供了两种分配的原语,即内建函数new和make。它们做的事情不同。
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分配内存
在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}
返回的是一个量的引用而非指针,所以最后返回时需要取地址符。
这个是很有必要注意的一件事,用为当你让函数中传入一个数组时,能不能改变外部数值的值呢?这就要考验到数值类型是值类型还是引用类型了。如果是引用类型的话,相当于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这个内联函数的。第七点会归纳这部分内联函数
这个函数比较神奇啊,我看官方文档的时候有些看不懂.官方文档(镜像网站上的中文官方文档)的一段
最后,每个源文件都可以通过定义自己的无参数 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它所在的包就执行。
这个我也是比较糊的,所以在这里进行了部分整理和试验。估计以后还有更多关于这点的问题
先把重要点记下:
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在第一点已有介绍了。编译通过,第二点分析合理!
这一部分也是比较乱的一点,关联这三个基本类型的内建函数大致可以分成三类。分别与创建、删除、操作。
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
slice1 = append(slice1,2,3,4)
slice2 = append(slice2,slice1...)
slice3 := append([]byte("hello"),"world"...)
//len(slice1)是5
//len(slice2)是3
//i==3,只会复制slice1的前三个元素到slice2中
i := copy(slice2,slice1)
//i==3,只会将slice2中的前三个元素复制到slice1中
i = copy(slice1,slice2)
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.
delete(mp,"啊啊啊")
close(ch)
append会返回一个切片,需要一个变量接收这个切片。
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)
}
在golang中字符串有两种直接遍历的方式
for range
遍历,得到的元素值时utf编码的rune类型for i := ...
的方式,得到的是byte类型所以因为这个差异,其上的两种方式遍历的长度也是不一样的,当然在utf和ascii码相同字符遍历时是没差别的。比如纯英文。
待续。。。
缘由: 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
syncthing
原版(web): https://github.com/syncthing/syncthing
Android: https://github.com/syncthing/syncthing-android
这边使用的是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使用的是源码安装,源码地址: 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程序目录名。
中文版下载网址: https://cn.wordpress.org/download/
将压缩包解压至/home/xx/nginx/build/html/
下, 这是index.php应该会发生替换,选择替换。
虽然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
~/.pam_environment
GTK_IM_MODULE DEFAULT=fcitx
QT_IM_MODULE DEFAULT=fcitx
XMODIFIERS DEFAULT=\@im=fcitx
后来发现,在kde环境下的那个系统之前安装时就已经修改该文件。
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 :
xhost +
dochar ------ docker-wechat:
curl -sL https://raw.githubusercontent.com/huan/docker-wechat/master/dochat.sh | bash
以上我无法执行,应该是墙的原因,所以直接吧shell脚本的文本复制出来运行。
脚本复制到本地的好处就是随时运行。
脚本我放在本仓库的linux/下
因为xhost +
在每次启动后都要执行,所以我这边的脚本里加了这一句了。
硬件环境: 树莓派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
sudo apt install dnsmasq hostapd
前者为dns和DHCP服务器。后者可以使无线网卡成为无线接入点。
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包连成一个通道(第五步骤会做)。
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频率的信号一样.
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的,其他都要和网卡信息符合,也就是你之前配置的。
介绍: 可以把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"
sudo vim /etc/sysctl.conf
----
将net.ipv4.ip_forward=1反注释掉,或者直接在最后输入
----
net.ipv4.ip_forward=1
或者直接以下操作
sudo echo 1 > /proc/sys/net/ipv4/ip_forward
使用 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
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
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)
其实就先设置临时环境变量在编译
renderer进程使用的是 https://github.com/GuoYuefei/simple_react_webpack 的第四次commit的模板
见 https://github.com/GuoYuefei/simple_react_webpack/blob/master/README.md
目录结构使用的是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
数据库安装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
核心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的内容。
先来看一个错误的程序
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类型定义, 空接口为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
}
相比 iface
,eface
就比较简单了。只维护了一个 _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
结构体,用两个指针来描述 itab
和 data
,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface
。最后就可以打印出动态类型和动态值的地址。
Output:
{0 0} {17426912 0} {17426912 842350714568}
5
a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int
;最后,c 的动态值为 5。
以上自定义inface强转的方法, 在以下参考资料的《Go 语言问题集》的‘标准库’的‘unsafe’部分有讲。是本好书,值得反复看。
先来上码,代码比较神奇,第一眼见他就深深的吸引了我。
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 内部,又没有调用一些诸如
channel
,time.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++时会需要考虑同步问题,导致效率低下。
效率相差比较大,以后编程时可以注意这方面,特别是有频繁的对一个变量读写时。
还有引起其他有趣的问题,有些还难以解释,有机会继续探索。。
ps. uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
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)))),
)
}
直接下以下网址下载
https://github.com/protocolbuffers/protobuf
然后将二进制和include放在path中
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
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
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")
}
}
来源: 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")
}
}
缘起于二刷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 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 1
c也是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指令)
以下为notion导出markdown之后并修改后的文档
现在主要流行的状态管理库redux和mobx。
redux大概的原理是用一个store来存储说有的状态,用action去定义修改类型,reducer定义如何修改(根据action),然后用dispatch去触发修改,subscribe去注册监听器,getState获取状态。
诶,本文重点不是redux和react,而是react-redux这个库。所以说吼,就只简单描述下redux了。记住redux很简洁,不与其他库有任何的关联之类的。在node中直接使用redux也是可以的,或者游览器环境下。
react-redux是react的连接桥梁,将redux作用到了react上。
api见链接:
api就两样东西,一个Provider组件,一个connect函数。
详细就看这份中文文档了,不赘(zhui4)述了。
在看到这份api后,我的第一想法是简单的实现一遍。
Provider可以让人想起react的context, connect可以让人想起高级组件的用法。
所以react-redux本身应该是用context,将redux生成的store传递到Provider,然后在connect中的包裹组件里使用this.context使用它。 context使用:
当然原生的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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.