Как лучше реализовать транзакцию?
В целом всё выглядит не плохо, но будет лучше, если контекст будем принимать на вход. Так же было не плохо отменять транзакцию в случае ошибки, ну и закрывать prepare тоже не помешает. На каждую итерацию цикла вовсе не обязательно содавать свой собственный prepare, достаточно одного

func (db *Requests) StorePhotos(ctx context.Context, photos []Photo) error {
	ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
	defer cancel()

	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	stmnt, err := tx.PrepareContext(ctx, "INSERT INTO photos (id, owner_id, req_id, url) VALUES ($1, $2, $3, $4)")
	if err != nil {
		return err
	}
	defer stmnt.Close()

	for _, photo := range photos {
		if _, err = stmnt.ExecContext(ctx, photo.ID, photo.OwnerID, photo.ReqID, photo.URL); err != nil {
			return err
		}
	}

	return tx.Commit()
}
insert into table (x,y,z) values (?,?,?), (?,?,?), (?,?,?), ...

а значения передавай как обычно сразу все, массивом, в правильном порядке